chore: update dependencies

This commit is contained in:
Samuel 2025-01-20 14:58:10 +01:00
parent 2d9f108ed2
commit 0e1eed664d
14 changed files with 1371 additions and 640 deletions

1
.npmrc Normal file
View file

@ -0,0 +1 @@
@duskflower:registry=https://git.duskflower.dev/api/packages/duskflower/npm/

View file

@ -108,13 +108,7 @@
} }
} }
}, },
"ignore": [ "ignore": ["dist/**/*.ts", "dist/**", "**/*.mjs", "eslint.config.js", "**/*.js"]
"dist/**/*.ts",
"dist/**",
"**/*.mjs",
"eslint.config.js",
"**/*.js"
]
}, },
"javascript": { "javascript": {
"formatter": { "formatter": {

View file

@ -15,30 +15,31 @@
"@biomejs/biome": "1.9.4", "@biomejs/biome": "1.9.4",
"@commitlint/cli": "^19.6.1", "@commitlint/cli": "^19.6.1",
"@commitlint/config-conventional": "^19.6.0", "@commitlint/config-conventional": "^19.6.0",
"@types/node": "^22.10.2", "@types/node": "^22.10.7",
"@types/sql.js": "^1.4.9", "@types/sql.js": "^1.4.9",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"better-sqlite3": "^11.7.0", "better-sqlite3": "^11.8.1",
"husky": "^9.1.7", "husky": "^9.1.7",
"kysely-codegen": "^0.17.0", "kysely-codegen": "^0.17.0",
"lint-staged": "^15.2.11", "lint-staged": "^15.4.1",
"postcss": "^8.4.49", "postcss": "^8.5.1",
"tailwindcss": "^3.4.17", "tailwindcss": "^3.4.17",
"typescript": "^5.7.2", "typescript": "^5.7.3",
"vite": "^6.0.6", "vite": "^6.0.9",
"vite-plugin-solid": "^2.11.0", "vite-plugin-solid": "^2.11.0",
"vite-plugin-wasm": "^3.4.1" "vite-plugin-wasm": "^3.4.1"
}, },
"dependencies": { "dependencies": {
"@duskflower/signal-decrypt-backup-wasm": "^0.2.0", "@duskflower/signal-decrypt-backup-wasm": "^0.2.1",
"@kobalte/core": "^0.13.7", "@kobalte/core": "^0.13.7",
"@kobalte/tailwindcss": "^0.9.0", "@kobalte/tailwindcss": "^0.9.0",
"@solid-primitives/refs": "^1.0.8", "@solid-primitives/refs": "^1.0.8",
"@solid-primitives/storage": "^4.2.1", "@solid-primitives/storage": "^4.2.1",
"@solid-primitives/upload": "^0.0.117",
"@solid-primitives/workers": "^0.3.0", "@solid-primitives/workers": "^0.3.0",
"@solidjs/meta": "^0.29.4", "@solidjs/meta": "^0.29.4",
"@solidjs/router": "^0.15.2", "@solidjs/router": "^0.15.3",
"@sqlite.org/sqlite-wasm": "3.47.2-build1", "@sqlite.org/sqlite-wasm": "3.48.0-build2",
"@tanstack/solid-table": "^8.20.5", "@tanstack/solid-table": "^8.20.5",
"chart.js": "^4.4.7", "chart.js": "^4.4.7",
"chartjs-chart-wordcloud": "^4.4.4", "chartjs-chart-wordcloud": "^4.4.4",
@ -49,9 +50,9 @@
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"kysely": "^0.27.5", "kysely": "^0.27.5",
"kysely-wasm": "^0.7.0", "kysely-wasm": "^0.7.0",
"lucide-solid": "^0.469.0", "lucide-solid": "^0.473.0",
"seroval": "^1.1.1", "seroval": "^1.2.0",
"solid-js": "^1.9.3", "solid-js": "^1.9.4",
"tailwind-merge": "^2.6.0", "tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7" "tailwindcss-animate": "^1.0.7"
}, },

871
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -1,12 +1,8 @@
import { sql, type NotNull } from "kysely"; import { sql, type NotNull } from "kysely";
import { db, kyselyDb, SELF_ID, setDbHash } from "./db"; import { db, kyselyDb, SELF_ID } from "./db";
import { cached } from "./lib/db-cache"; import { cached } from "../lib/db-cache";
import { hashString } from "./lib/hash";
export const loadDb = ( export const loadDb = (statements: string[], progressCallback?: (percentage: number) => void) => {
statements: string[],
progressCallback?: (percentage: number) => void,
) => {
const length = statements.length; const length = statements.length;
let percentage = 0; let percentage = 0;
@ -15,20 +11,19 @@ export const loadDb = (
const newPercentage = Math.round((i / length) * 100); const newPercentage = Math.round((i / length) * 100);
try { try {
db.exec(statement); if (progressCallback && newPercentage !== percentage) {
progressCallback(newPercentage);
if (newPercentage !== percentage) {
progressCallback?.(newPercentage);
percentage = newPercentage; percentage = newPercentage;
} }
db.exec(statement);
} catch (e) { } catch (e) {
throw new Error(`statement failed: ${statement}`, { throw new Error(`statement failed: ${statement}`, {
cause: e, cause: e,
}); });
} }
} }
setDbHash(hashString(statements.join()));
}; };
const allThreadsOverviewQueryRaw = () => const allThreadsOverviewQueryRaw = () =>
@ -38,15 +33,9 @@ const allThreadsOverviewQueryRaw = () =>
(eb) => (eb) =>
eb eb
.selectFrom("message") .selectFrom("message")
.select((eb) => [ .select((eb) => ["message.thread_id", eb.fn.countAll().as("message_count")])
"message.thread_id",
eb.fn.countAll().as("message_count"),
])
.where((eb) => { .where((eb) => {
return eb.and([ return eb.and([eb("message.body", "is not", null), eb("message.body", "is not", "")]);
eb("message.body", "is not", null),
eb("message.body", "is not", ""),
]);
}) })
.groupBy("message.thread_id") .groupBy("message.thread_id")
.as("message"), .as("message"),
@ -100,9 +89,7 @@ const dmPartnerRecipientQueryRaw = (dmId: number) =>
"recipient.nickname_joined_name", "recipient.nickname_joined_name",
]) ])
.innerJoin("thread", "recipient._id", "thread.recipient_id") .innerJoin("thread", "recipient._id", "thread.recipient_id")
.where((eb) => .where((eb) => eb.and([eb("thread._id", "=", dmId), eb("recipient._id", "!=", SELF_ID)]))
eb.and([eb("thread._id", "=", dmId), eb("recipient._id", "!=", SELF_ID)]),
)
.$narrowType<{ .$narrowType<{
_id: number; _id: number;
}>() }>()
@ -113,25 +100,12 @@ export const dmPartnerRecipientQuery = cached(dmPartnerRecipientQueryRaw);
const threadSentMessagesOverviewQueryRaw = (threadId: number) => const threadSentMessagesOverviewQueryRaw = (threadId: number) =>
kyselyDb kyselyDb
.selectFrom("message") .selectFrom("message")
.select([ .select(["from_recipient_id", sql<string>`datetime(date_sent / 1000, 'unixepoch')`.as("message_datetime")])
"from_recipient_id",
sql<string>`datetime(date_sent / 1000, 'unixepoch')`.as(
"message_datetime",
),
])
.orderBy(["message_datetime"]) .orderBy(["message_datetime"])
.where((eb) => .where((eb) => eb.and([eb("body", "is not", null), eb("body", "!=", ""), eb("thread_id", "=", threadId)]))
eb.and([
eb("body", "is not", null),
eb("body", "!=", ""),
eb("thread_id", "=", threadId),
]),
)
.execute(); .execute();
export const threadSentMessagesOverviewQuery = cached( export const threadSentMessagesOverviewQuery = cached(threadSentMessagesOverviewQueryRaw);
threadSentMessagesOverviewQueryRaw,
);
const threadMostUsedWordsQueryRaw = (threadId: number, limit = 10) => const threadMostUsedWordsQueryRaw = (threadId: number, limit = 10) =>
kyselyDb kyselyDb
@ -139,20 +113,16 @@ const threadMostUsedWordsQueryRaw = (threadId: number, limit = 10) =>
return eb return eb
.selectFrom("message") .selectFrom("message")
.select([ .select([
sql`LOWER(substr(body, 1, instr(body || " ", " ") - 1))`.as("word"), sql`LOWER(substr(body, 1, instr(body || ' ', ' ') - 1))`.as("word"),
sql`(substr(body, instr(body || " ", " ") + 1))`.as("rest"), sql`substr(body, instr(body || ' ', ' ') + 1)`.as("rest"),
]) ])
.where((eb) => .where((eb) => eb.and([eb("body", "is not", null), eb("thread_id", "=", threadId)]))
eb.and([eb("body", "is not", null), eb("thread_id", "=", threadId)]),
)
.unionAll((ebInner) => { .unionAll((ebInner) => {
return ebInner return ebInner
.selectFrom("words") .selectFrom("words")
.select([ .select([
sql`LOWER(substr(rest, 1, instr(rest || " ", " ") - 1))`.as( sql`LOWER(substr(rest, 1, instr(rest || ' ', ' ') - 1))`.as("word"),
"word", sql`substr(rest, instr(rest || ' ', ' ') + 1)`.as("rest"),
),
sql`(substr(rest, instr(rest || " ", " ") + 1))`.as("rest"),
]) ])
.where("rest", "<>", ""); .where("rest", "<>", "");
}); });

708
src/db/db-schema.d.ts vendored Normal file
View file

@ -0,0 +1,708 @@
/**
* This file was generated by kysely-codegen.
* Please do not edit it manually.
*/
import type { ColumnType } from "kysely";
export type Generated<T> = T extends ColumnType<infer S, infer I, infer U>
? ColumnType<S, I | undefined, U>
: ColumnType<T, T | undefined, T>;
export interface Attachment {
_id: Generated<number | null>;
archive_cdn: Generated<number | null>;
archive_media_id: Generated<string | null>;
archive_media_name: Generated<string | null>;
archive_thumbnail_media_id: Generated<string | null>;
archive_transfer_file: Generated<string | null>;
archive_transfer_state: Generated<number | null>;
attachment_uuid: Generated<string | null>;
blur_hash: Generated<string | null>;
borderless: Generated<number | null>;
caption: Generated<string | null>;
cdn_number: Generated<number | null>;
content_type: string | null;
data_file: string | null;
data_hash_end: Generated<string | null>;
data_hash_start: Generated<string | null>;
data_random: Buffer | null;
data_size: number | null;
display_order: Generated<number | null>;
fast_preflight_id: string | null;
file_name: string | null;
height: Generated<number | null>;
message_id: number | null;
offload_restored_at: Generated<number | null>;
quote: Generated<number | null>;
remote_digest: Buffer | null;
remote_incremental_digest: Buffer | null;
remote_incremental_digest_chunk_size: Generated<number | null>;
remote_iv: Generated<Buffer | null>;
remote_key: string | null;
remote_location: string | null;
sticker_emoji: Generated<string | null>;
sticker_id: Generated<number | null>;
sticker_pack_id: Generated<string | null>;
sticker_pack_key: Generated<string | null>;
thumbnail_file: Generated<string | null>;
thumbnail_random: Generated<Buffer | null>;
thumbnail_restore_state: Generated<number | null>;
transfer_file: Generated<string | null>;
transfer_state: number | null;
transform_properties: Generated<string | null>;
upload_timestamp: Generated<number | null>;
video_gif: Generated<number | null>;
voice_note: Generated<number | null>;
width: Generated<number | null>;
}
export interface AvatarPicker {
_id: Generated<number | null>;
avatar: Buffer;
group_id: Generated<string | null>;
last_used: Generated<number | null>;
}
export interface Call {
_id: number | null;
call_id: number;
deletion_timestamp: Generated<number | null>;
direction: number;
event: number;
group_call_active: Generated<number | null>;
local_joined: Generated<number | null>;
message_id: Generated<number | null>;
peer: number;
read: Generated<number | null>;
ringer: Generated<number | null>;
timestamp: number;
type: number;
}
export interface CallLink {
_id: number | null;
admin_key: Buffer | null;
deletion_timestamp: Generated<number>;
expiration: number;
name: string;
recipient_id: number | null;
restrictions: number;
revoked: number;
room_id: string;
root_key: Buffer | null;
}
export interface Cds {
_id: number | null;
e164: string;
last_seen_at: Generated<number | null>;
}
export interface ChatColors {
_id: Generated<number | null>;
chat_colors: Buffer | null;
}
export interface ChatFolder {
_id: Generated<number | null>;
folder_type: Generated<number | null>;
is_muted: Generated<number | null>;
name: Generated<string | null>;
position: Generated<number | null>;
show_groups: Generated<number | null>;
show_individual: Generated<number | null>;
show_muted: Generated<number | null>;
show_unread: Generated<number | null>;
}
export interface ChatFolderMembership {
_id: Generated<number | null>;
chat_folder_id: number;
membership_type: Generated<number | null>;
thread_id: number;
}
export interface DistributionList {
_id: Generated<number | null>;
allows_replies: Generated<number | null>;
deletion_timestamp: Generated<number | null>;
distribution_id: string;
is_unknown: Generated<number | null>;
name: string;
privacy_mode: Generated<number | null>;
recipient_id: number | null;
}
export interface DistributionListMember {
_id: Generated<number | null>;
list_id: number;
privacy_mode: Generated<number | null>;
recipient_id: number;
}
export interface DonationReceipt {
_id: Generated<number | null>;
amount: string;
currency: string;
receipt_date: number;
receipt_type: string;
subscription_level: number;
}
export interface Drafts {
_id: number | null;
thread_id: number | null;
type: string | null;
value: string | null;
}
export interface EmojiSearch {
_id: number | null;
emoji: string;
label: string;
rank: Generated<number | null>;
}
export interface GroupMembership {
_id: number | null;
endorsement: Generated<Buffer | null>;
group_id: string;
recipient_id: number;
}
export interface GroupReceipts {
_id: number | null;
address: number | null;
mms_id: number | null;
status: number | null;
timestamp: number | null;
unidentified: Generated<number | null>;
}
export interface Groups {
_id: number | null;
active: Generated<number | null>;
avatar_content_type: Generated<string | null>;
avatar_digest: Generated<Buffer | null>;
avatar_id: Generated<number | null>;
avatar_key: Generated<Buffer | null>;
decrypted_group: Generated<Buffer | null>;
distribution_id: Generated<string | null>;
expected_v2_id: Generated<string | null>;
group_id: string;
group_send_endorsements_expiration: Generated<number | null>;
last_force_update_timestamp: Generated<number | null>;
master_key: Generated<Buffer | null>;
mms: Generated<number | null>;
recipient_id: number;
revision: Generated<Buffer | null>;
show_as_story_state: Generated<number | null>;
timestamp: Generated<number | null>;
title: Generated<string | null>;
unmigrated_v1_members: Generated<string | null>;
}
export interface Identities {
_id: Generated<number | null>;
address: number | null;
first_use: Generated<number | null>;
identity_key: string | null;
nonblocking_approval: Generated<number | null>;
timestamp: Generated<number | null>;
verified: Generated<number | null>;
}
export interface InAppPayment {
_id: number | null;
data: Buffer;
end_of_period: Generated<number | null>;
inserted_at: number;
notified: Generated<number | null>;
state: number;
subscriber_id: string | null;
type: number;
updated_at: number;
}
export interface InAppPaymentSubscriber {
_id: number | null;
currency_code: string;
payment_method_type: Generated<number | null>;
requires_cancel: Generated<number | null>;
subscriber_id: string;
type: number;
}
export interface KyberPrekey {
_id: number | null;
account_id: string;
key_id: number;
last_resort: number;
serialized: Buffer;
stale_timestamp: Generated<number>;
timestamp: number;
}
export interface Mention {
_id: Generated<number | null>;
message_id: number | null;
range_length: number | null;
range_start: number | null;
recipient_id: number | null;
thread_id: number | null;
}
export interface Message {
_id: Generated<number | null>;
body: string | null;
ct_l: string | null;
date_received: number;
date_sent: number;
date_server: Generated<number | null>;
exp: number | null;
expire_started: Generated<number | null>;
expire_timer_version: Generated<number>;
expires_in: Generated<number | null>;
export_state: Generated<Buffer | null>;
exported: Generated<number | null>;
from_device_id: number | null;
from_recipient_id: number;
has_delivery_receipt: Generated<number | null>;
has_read_receipt: Generated<number | null>;
latest_revision_id: Generated<number | null>;
link_previews: Generated<string | null>;
m_size: number | null;
m_type: number | null;
mentions_self: Generated<number | null>;
message_extras: Generated<Buffer | null>;
message_ranges: Generated<Buffer | null>;
mismatched_identities: Generated<string | null>;
network_failures: Generated<string | null>;
notified: Generated<number | null>;
notified_timestamp: Generated<number | null>;
original_message_id: Generated<number | null>;
parent_story_id: Generated<number | null>;
quote_author: Generated<number | null>;
quote_body: Generated<string | null>;
quote_id: Generated<number | null>;
quote_mentions: Generated<Buffer | null>;
quote_missing: Generated<number | null>;
quote_type: Generated<number | null>;
reactions_last_seen: Generated<number | null>;
reactions_unread: Generated<number | null>;
read: Generated<number | null>;
receipt_timestamp: Generated<number | null>;
remote_deleted: Generated<number | null>;
revision_number: Generated<number | null>;
scheduled_date: Generated<number | null>;
server_guid: Generated<string | null>;
shared_contacts: Generated<string | null>;
st: number | null;
story_type: Generated<number | null>;
subscription_id: Generated<number | null>;
thread_id: number;
to_recipient_id: number;
tr_id: string | null;
type: number;
unidentified: Generated<number | null>;
view_once: Generated<number | null>;
viewed: Generated<number | null>;
}
export interface MessageFts {
body: string | null;
thread_id: string | null;
}
export interface MessageFtsConfig {
k: string;
v: string | null;
}
export interface MessageFtsData {
block: Buffer | null;
id: number | null;
}
export interface MessageFtsDocsize {
id: number | null;
sz: Buffer | null;
}
export interface MessageFtsIdx {
pgno: string | null;
segid: string;
term: string;
}
export interface MslMessage {
_id: number | null;
message_id: number;
payload_id: number;
}
export interface MslPayload {
_id: number | null;
content: Buffer;
content_hint: number;
date_sent: number;
urgent: Generated<number>;
}
export interface MslRecipient {
_id: number | null;
device: number;
payload_id: number;
recipient_id: number;
}
export interface NameCollision {
_id: Generated<number | null>;
dismissed: Generated<number | null>;
hash: Generated<string | null>;
thread_id: number;
}
export interface NameCollisionMembership {
_id: Generated<number | null>;
collision_id: number;
profile_change_details: Generated<Buffer | null>;
recipient_id: number;
}
export interface NotificationProfile {
_id: Generated<number | null>;
allow_all_calls: Generated<number>;
allow_all_mentions: Generated<number>;
color: string;
created_at: number;
emoji: string;
name: string;
}
export interface NotificationProfileAllowedMembers {
_id: Generated<number | null>;
notification_profile_id: number;
recipient_id: number;
}
export interface NotificationProfileSchedule {
_id: Generated<number | null>;
days_enabled: string;
enabled: Generated<number>;
end: number;
notification_profile_id: number;
start: number;
}
export interface OneTimePrekeys {
_id: number | null;
account_id: string;
key_id: number;
private_key: string;
public_key: string;
stale_timestamp: Generated<number>;
}
export interface Payments {
_id: number | null;
amount: Buffer;
block_index: Generated<number | null>;
block_timestamp: Generated<number | null>;
direction: number | null;
failure_reason: number | null;
fee: Buffer;
note: Generated<string | null>;
payment_metadata: Generated<Buffer | null>;
receipt: Generated<Buffer | null>;
receipt_public_key: Generated<string | null>;
recipient: Generated<number | null>;
recipient_address: Generated<string | null>;
seen: number | null;
state: number | null;
timestamp: number | null;
transaction_record: Generated<Buffer | null>;
uuid: Generated<string | null>;
}
export interface PendingPniSignatureMessage {
_id: number | null;
device_id: number;
recipient_id: number;
sent_timestamp: number;
}
export interface PendingRetryReceipts {
_id: Generated<number | null>;
author: string;
device: number;
received_timestamp: string;
sent_timestamp: number;
thread_id: number;
}
export interface Reaction {
_id: number | null;
author_id: number;
date_received: number;
date_sent: number;
emoji: string;
message_id: number;
}
export interface Recipient {
_id: Generated<number | null>;
about: Generated<string | null>;
about_emoji: Generated<string | null>;
aci: Generated<string | null>;
avatar_color: Generated<string | null>;
badges: Generated<Buffer | null>;
blocked: Generated<number | null>;
call_link_room_id: Generated<string | null>;
call_ringtone: Generated<string | null>;
call_vibrate: Generated<number | null>;
capabilities: Generated<number | null>;
chat_colors: Generated<Buffer | null>;
custom_chat_colors_id: Generated<number | null>;
distribution_list_id: Generated<number | null>;
e164: Generated<string | null>;
email: Generated<string | null>;
extras: Generated<Buffer | null>;
group_id: Generated<string | null>;
groups_in_common: Generated<number | null>;
hidden: Generated<number | null>;
last_profile_fetch: Generated<number | null>;
last_session_reset: Generated<Buffer | null>;
mention_setting: Generated<number | null>;
message_expiration_time: Generated<number | null>;
message_expiration_time_version: Generated<number>;
message_ringtone: Generated<string | null>;
message_vibrate: Generated<number | null>;
mute_until: Generated<number | null>;
needs_pni_signature: Generated<number | null>;
nickname_family_name: Generated<string | null>;
nickname_given_name: Generated<string | null>;
nickname_joined_name: Generated<string | null>;
note: Generated<string | null>;
notification_channel: Generated<string | null>;
phone_number_discoverable: Generated<number | null>;
phone_number_sharing: Generated<number | null>;
pni: Generated<string | null>;
pni_signature_verified: Generated<number | null>;
profile_avatar: Generated<string | null>;
profile_family_name: Generated<string | null>;
profile_given_name: Generated<string | null>;
profile_joined_name: Generated<string | null>;
profile_key: Generated<string | null>;
profile_key_credential: Generated<string | null>;
profile_sharing: Generated<number | null>;
registered: Generated<number | null>;
reporting_token: Generated<Buffer | null>;
sealed_sender_mode: Generated<number | null>;
storage_service_id: Generated<string | null>;
storage_service_proto: Generated<string | null>;
system_contact_uri: Generated<string | null>;
system_family_name: Generated<string | null>;
system_given_name: Generated<string | null>;
system_info_pending: Generated<number | null>;
system_joined_name: Generated<string | null>;
system_nickname: Generated<string | null>;
system_phone_label: Generated<string | null>;
system_phone_type: Generated<number | null>;
system_photo_uri: Generated<string | null>;
type: Generated<number | null>;
unregistered_timestamp: Generated<number | null>;
username: Generated<string | null>;
wallpaper: Generated<Buffer | null>;
wallpaper_uri: Generated<string | null>;
}
export interface RemappedRecipients {
_id: Generated<number | null>;
new_id: number | null;
old_id: number | null;
}
export interface RemappedThreads {
_id: Generated<number | null>;
new_id: number | null;
old_id: number | null;
}
export interface RemoteMegaphone {
_id: number | null;
body: string;
conditional_id: string | null;
countries: string | null;
dont_show_after: number;
dont_show_before: number;
finished_at: Generated<number | null>;
image_uri: Generated<string | null>;
image_url: string | null;
minimum_version: number;
primary_action_data: Generated<string | null>;
primary_action_id: string | null;
primary_action_text: string | null;
priority: number;
secondary_action_data: Generated<string | null>;
secondary_action_id: string | null;
secondary_action_text: string | null;
seen_count: Generated<number | null>;
show_for_days: number;
shown_at: Generated<number | null>;
snoozed_at: Generated<number | null>;
title: string;
uuid: string;
}
export interface SenderKeys {
_id: Generated<number | null>;
address: string;
created_at: number;
device: number;
distribution_id: string;
record: Buffer;
}
export interface SenderKeyShared {
_id: Generated<number | null>;
address: string;
device: number;
distribution_id: string;
timestamp: Generated<number | null>;
}
export interface Sessions {
_id: Generated<number | null>;
account_id: string;
address: string;
device: number;
record: Buffer;
}
export interface SignedPrekeys {
_id: number | null;
account_id: string;
key_id: number;
private_key: string;
public_key: string;
signature: string;
timestamp: Generated<number | null>;
}
export interface Sticker {
_id: Generated<number | null>;
content_type: Generated<string | null>;
cover: number | null;
emoji: string;
file_length: number | null;
file_path: string;
file_random: Buffer | null;
installed: number | null;
last_used: number | null;
pack_author: string;
pack_id: string;
pack_key: string;
pack_order: number | null;
pack_title: string;
sticker_id: number | null;
}
export interface StorageKey {
_id: Generated<number | null>;
key: string | null;
type: number | null;
}
export interface StorySends {
_id: number | null;
allows_replies: number;
distribution_id: string;
message_id: number;
recipient_id: number;
sent_timestamp: number;
}
export interface Thread {
_id: Generated<number | null>;
active: Generated<number | null>;
archived: Generated<number | null>;
date: Generated<number | null>;
error: Generated<number | null>;
expires_in: Generated<number | null>;
has_delivery_receipt: Generated<number | null>;
has_read_receipt: Generated<number | null>;
has_sent: Generated<number | null>;
last_scrolled: Generated<number | null>;
last_seen: Generated<number | null>;
meaningful_messages: Generated<number | null>;
pinned: Generated<number | null>;
read: Generated<number | null>;
recipient_id: number;
snippet: string | null;
snippet_content_type: Generated<string | null>;
snippet_extras: Generated<string | null>;
snippet_message_extras: Generated<Buffer | null>;
snippet_type: Generated<number | null>;
snippet_uri: Generated<string | null>;
status: Generated<number | null>;
type: Generated<number | null>;
unread_count: Generated<number | null>;
unread_self_mention_count: Generated<number | null>;
}
export interface DB {
attachment: Attachment;
avatar_picker: AvatarPicker;
call: Call;
call_link: CallLink;
cds: Cds;
chat_colors: ChatColors;
chat_folder: ChatFolder;
chat_folder_membership: ChatFolderMembership;
distribution_list: DistributionList;
distribution_list_member: DistributionListMember;
donation_receipt: DonationReceipt;
drafts: Drafts;
emoji_search: EmojiSearch;
group_membership: GroupMembership;
group_receipts: GroupReceipts;
groups: Groups;
identities: Identities;
in_app_payment: InAppPayment;
in_app_payment_subscriber: InAppPaymentSubscriber;
kyber_prekey: KyberPrekey;
mention: Mention;
message: Message;
message_fts: MessageFts;
message_fts_config: MessageFtsConfig;
message_fts_data: MessageFtsData;
message_fts_docsize: MessageFtsDocsize;
message_fts_idx: MessageFtsIdx;
msl_message: MslMessage;
msl_payload: MslPayload;
msl_recipient: MslRecipient;
name_collision: NameCollision;
name_collision_membership: NameCollisionMembership;
notification_profile: NotificationProfile;
notification_profile_allowed_members: NotificationProfileAllowedMembers;
notification_profile_schedule: NotificationProfileSchedule;
one_time_prekeys: OneTimePrekeys;
payments: Payments;
pending_pni_signature_message: PendingPniSignatureMessage;
pending_retry_receipts: PendingRetryReceipts;
reaction: Reaction;
recipient: Recipient;
remapped_recipients: RemappedRecipients;
remapped_threads: RemappedThreads;
remote_megaphone: RemoteMegaphone;
sender_key_shared: SenderKeyShared;
sender_keys: SenderKeys;
sessions: Sessions;
signed_prekeys: SignedPrekeys;
sticker: Sticker;
storage_key: StorageKey;
story_sends: StorySends;
thread: Thread;
}

View file

@ -1,7 +1,7 @@
import { makePersisted } from "@solid-primitives/storage"; import { makePersisted } from "@solid-primitives/storage";
import sqlite3InitModule from "@sqlite.org/sqlite-wasm"; import sqlite3InitModule from "@sqlite.org/sqlite-wasm";
import { Kysely } from "kysely"; import { Kysely } from "kysely";
import type { DB } from "kysely-codegen"; import type { DB } from "./db-schema";
import { OfficialWasmDialect } from "kysely-wasm"; import { OfficialWasmDialect } from "kysely-wasm";
import { createSignal } from "solid-js"; import { createSignal } from "solid-js";

2
src/db/index.ts Normal file
View file

@ -0,0 +1,2 @@
export * from "./db";
export * from "./db-queries";

View file

@ -1,9 +1,13 @@
/* @refresh reload */ /* @refresh reload */
import { MetaProvider } from "@solidjs/meta"; import { MetaProvider } from "@solidjs/meta";
import { Router } from "@solidjs/router"; import { Router, useNavigate } from "@solidjs/router";
import { render } from "solid-js/web"; import { render } from "solid-js/web";
import { createEffect, enableScheduling } from "solid-js";
import App from "./App"; import App from "./App";
import { db } from "./db/db";
enableScheduling();
const root = document.getElementById("root"); const root = document.getElementById("root");
@ -19,18 +23,18 @@ if (root) {
<div class="mx-auto max-w-screen-2xl"> <div class="mx-auto max-w-screen-2xl">
<MetaProvider> <MetaProvider>
<Router <Router
// root={(props) => { root={(props) => {
// const navigate = useNavigate(); const navigate = useNavigate();
// const { pathname } = props.location; const { pathname } = props.location;
// createEffect(() => { createEffect(() => {
// if (!db() && pathname !== "/") { if (!db && pathname !== "/") {
// navigate("/"); navigate("/");
// } }
// }); });
// return props.children; return props.children;
// }} }}
> >
<App /> <App />
</Router> </Router>

View file

@ -1,47 +1,44 @@
import { deserialize, serialize } from "seroval"; import { deserialize, serialize } from "seroval";
import { createEffect, createMemo, createRoot, on } from "solid-js"; import {} from "solid-js";
import { dbHash } from "~/db";
import { hashString } from "./hash"; import { hashString } from "./hash";
export const DATABASE_HASH_PREFIX = "database"; export const DATABASE_HASH_PREFIX = "database";
// clear the cache on new session so that selecting a different database does not result in wrong cache entries // clear the cache on new session so that selecting a different database does not result in wrong cache entries
const clearDbCache = () => { // const clearDbCache = () => {
for (let i = 0, len = localStorage.length; i < len; i++) { // for (let i = 0, len = localStorage.length; i < len; i++) {
const key = localStorage.key(i); // const key = localStorage.key(i);
if (key?.startsWith(DATABASE_HASH_PREFIX)) { // if (key?.startsWith(DATABASE_HASH_PREFIX)) {
localStorage.removeItem(key); // localStorage.removeItem(key);
} // }
} // }
}; // };
let prevDbHash = dbHash(); // let prevDbHash = dbHash();
createRoot(() => { // createRoot(() => {
createEffect(() => { // createEffect(() => {
on( // on(
dbHash, // dbHash,
(currentDbHash) => { // (currentDbHash) => {
if (currentDbHash && currentDbHash !== prevDbHash) { // if (currentDbHash && currentDbHash !== prevDbHash) {
prevDbHash = currentDbHash; // prevDbHash = currentDbHash;
clearDbCache(); // clearDbCache();
} // }
}, // },
{ // {
defer: true, // defer: true,
}, // },
); // );
}); // });
}); // });
class LocalStorageCacheAdapter { class LocalStorageCacheAdapter {
keys = new Set<string>( keys = new Set<string>(Object.keys(localStorage).filter((key) => key.startsWith(this.prefix)));
Object.keys(localStorage).filter((key) => key.startsWith(this.prefix)),
);
prefix = "database"; prefix = "database";
// TODO: real way of detecting if the db is loaded, on loading the db and opfs (if persisted db?) // TODO: real way of detecting if the db is loaded, on loading the db and opfs (if persisted db?)
#dbLoaded = createMemo(() => !!dbHash()); // #dbLoaded = createMemo(() => !!dbHash());
#createKey(cacheName: string, key: string): string { #createKey(cacheName: string, key: string): string {
return `${this.prefix}-${cacheName}-${key}`; return `${this.prefix}-${cacheName}-${key}`;
@ -54,10 +51,7 @@ class LocalStorageCacheAdapter {
try { try {
localStorage.setItem(fullKey, serialize({ isPromise, value })); localStorage.setItem(fullKey, serialize({ isPromise, value }));
} catch (error: unknown) { } catch (error: unknown) {
if ( if (error instanceof DOMException && error.name === "QUOTA_EXCEEDED_ERR") {
error instanceof DOMException &&
error.name === "QUOTA_EXCEEDED_ERR"
) {
console.error("Storage quota exceeded, not caching new function calls"); console.error("Storage quota exceeded, not caching new function calls");
} else { } else {
console.error(error); console.error(error);
@ -66,13 +60,13 @@ class LocalStorageCacheAdapter {
} }
has(cacheName: string, key: string): boolean { has(cacheName: string, key: string): boolean {
if (this.#dbLoaded()) { // if (this.#dbLoaded()) {
return this.keys.has(this.#createKey(cacheName, key)); return this.keys.has(this.#createKey(cacheName, key));
} // }
console.info("No database loaded"); // console.info("No database loaded");
return false; // return false;
} }
get<R>( get<R>(
@ -84,7 +78,7 @@ class LocalStorageCacheAdapter {
value: R; value: R;
} }
| undefined { | undefined {
if (this.#dbLoaded()) { // if (this.#dbLoaded()) {
const item = localStorage.getItem(this.#createKey(cacheName, key)); const item = localStorage.getItem(this.#createKey(cacheName, key));
if (item) { if (item) {
@ -93,9 +87,9 @@ class LocalStorageCacheAdapter {
value: R; value: R;
}; };
} }
} else { // } else {
console.info("No database loaded"); // console.info("No database loaded");
} // }
} }
} }
@ -128,10 +122,7 @@ const createHashKey = (...args: unknown[]) => {
return hashString(stringToHash); return hashString(stringToHash);
}; };
export const cached = <T extends unknown[], R, TT>( export const cached = <T extends unknown[], R, TT>(fn: (...args: T) => R, self?: ThisType<TT>): ((...args: T) => R) => {
fn: (...args: T) => R,
self?: ThisType<TT>,
): ((...args: T) => R) => {
const cacheName = hashString(fn.toString()).toString(); const cacheName = hashString(fn.toString()).toString();
return (...args: T) => { return (...args: T) => {
@ -140,11 +131,7 @@ export const cached = <T extends unknown[], R, TT>(
const cachedValue = cache.get<R>(cacheName, cacheKey); const cachedValue = cache.get<R>(cacheName, cacheKey);
if (cachedValue) { if (cachedValue) {
return ( return (cachedValue.isPromise ? Promise.resolve(cachedValue.value) : cachedValue.value) as R;
cachedValue.isPromise
? Promise.resolve(cachedValue.value)
: cachedValue.value
) as R;
} }
let newValue: R; let newValue: R;

View file

@ -1,7 +1,7 @@
import { Suspense, type Component } from "solid-js"; import { Suspense, type Component } from "solid-js";
import { createAsync, type RoutePreloadFunc, type RouteSectionProps } from "@solidjs/router"; import { createAsync, type RoutePreloadFunc, type RouteSectionProps } from "@solidjs/router";
import { dmPartnerRecipientQuery, threadMostUsedWordsQuery, threadSentMessagesOverviewQuery } from "~/db-queries"; import { dmPartnerRecipientQuery, threadMostUsedWordsQuery, threadSentMessagesOverviewQuery, SELF_ID } from "~/db";
import { getNameFromRecipient } from "~/lib/get-name-from-recipient"; import { getNameFromRecipient } from "~/lib/get-name-from-recipient";
import { Heading } from "~/components/ui/heading"; import { Heading } from "~/components/ui/heading";
import { Grid } from "~/components/ui/grid"; import { Grid } from "~/components/ui/grid";
@ -15,7 +15,6 @@ import { DmMessagesPerRecipient } from "./dm-messages-per-recipients";
import { DmMessagesPerWeekday } from "./dm-messages-per-weekday"; import { DmMessagesPerWeekday } from "./dm-messages-per-weekday";
import type { MessageOverview } from "~/types"; import type { MessageOverview } from "~/types";
import { createMessageStatsSources } from "~/lib/messages"; import { createMessageStatsSources } from "~/lib/messages";
import { SELF_ID } from "~/db";
import { Flex } from "~/components/ui/flex"; import { Flex } from "~/components/ui/flex";
const getDmIdData = (dmId: number) => { const getDmIdData = (dmId: number) => {

View file

@ -1,7 +1,7 @@
import type { ChartData } from "chart.js"; import type { ChartData } from "chart.js";
import { Show, type Accessor, type Component } from "solid-js"; import { Show, type Accessor, type Component } from "solid-js";
import { WordCloudChart } from "~/components/ui/charts"; import { WordCloudChart } from "~/components/ui/charts";
import type { threadMostUsedWordsQuery } from "~/db-queries"; import type { threadMostUsedWordsQuery } from "~/db";
const maxWordSize = 100; const maxWordSize = 100;

View file

@ -1,41 +1,60 @@
import { useNavigate, type RouteSectionProps } from "@solidjs/router"; import { useNavigate, type RouteSectionProps } from "@solidjs/router";
import { createSignal, Show, type Component, type JSX } from "solid-js"; import { createSignal, type JSX, Show, type Component } from "solid-js";
import { Title } from "@solidjs/meta"; import { Title } from "@solidjs/meta";
import { Portal } from "solid-js/web"; import { Portal } from "solid-js/web";
import { Flex } from "~/components/ui/flex"; import { Flex } from "~/components/ui/flex";
import { import { Progress, ProgressLabel, ProgressValueLabel } from "~/components/ui/progress";
Progress, import { loadDb } from "~/db/db-queries";
ProgressLabel,
ProgressValueLabel,
} from "~/components/ui/progress";
// import { db } from "~/db";
import { loadDb } from "~/db-queries";
import { decryptBackup } from "~/lib/decryptor"; import { decryptBackup } from "~/lib/decryptor";
import { createDropzone, createFileUploader } from "@solid-primitives/upload";
import { Button } from "~/components/ui/button";
import { TextField, TextFieldInput, TextFieldLabel } from "~/components/ui/text-field";
export const Home: Component<RouteSectionProps> = () => { export const Home: Component<RouteSectionProps> = () => {
const [decryptionProgress, setDecryptionProgress] = createSignal<number>();
const [isLoadingDatabase, setIsLoadingDatabase] = createSignal(false);
const [passphrase, setPassphrase] = createSignal("");
const navigate = useNavigate(); const navigate = useNavigate();
const onFileChange: JSX.ChangeEventHandler<HTMLInputElement, Event> = ( const fileUploader = createFileUploader({
event, accept: ".backup",
) => { multiple: false,
const file = event.currentTarget.files?.[0]; });
const dropzone = createDropzone({
onDrop: (files) => {
const file = files.at(0);
if (file?.name.endsWith(".backup")) {
setBackupFile(file.file);
}
},
});
const [passphrase, setPassphrase] = createSignal("");
const [backupFile, setBackupFile] = createSignal<File>();
const [decryptionProgress, setDecryptionProgress] = createSignal<number>();
const [loadingProgress, setLoadingProgress] = createSignal<number>();
// const [isLoadingDatabase, setIsLoadingDatabase] = createSignal(false);
const onSubmit: JSX.EventHandler<HTMLFormElement, SubmitEvent> = (event) => {
event.preventDefault();
const currentBackupFile = backupFile();
const currentPassphrase = passphrase(); const currentPassphrase = passphrase();
if (file && currentPassphrase) { if (currentBackupFile && currentPassphrase) {
decryptBackup(file, currentPassphrase, setDecryptionProgress) decryptBackup(currentBackupFile, currentPassphrase, setDecryptionProgress)
.then((result) => { .then((result) => {
setDecryptionProgress(undefined); setDecryptionProgress(undefined);
setIsLoadingDatabase(true); // setIsLoadingDatabase(true);
setLoadingProgress(0);
setTimeout(() => { setTimeout(() => {
loadDb(result.database_statements); loadDb(result.database_statements, (newValue) => (console.log(newValue), setLoadingProgress(newValue)));
setIsLoadingDatabase(false); // setIsLoadingDatabase(false);
setLoadingProgress(undefined);
navigate("/overview"); navigate("/overview");
}, 0); }, 0);
@ -53,9 +72,10 @@ export const Home: Component<RouteSectionProps> = () => {
flexDirection="col" flexDirection="col"
alignItems="center" alignItems="center"
justifyContent="center" justifyContent="center"
class="fixed inset-0 backdrop-blur-lg backdrop-filter gap-y-8" class="fixed inset-0 gap-y-8 backdrop-blur-lg backdrop-filter"
classList={{ classList={{
hidden: decryptionProgress() === undefined && !isLoadingDatabase(), // hidden: decryptionProgress() === undefined && !isLoadingDatabase(),
hidden: decryptionProgress() === undefined && loadingProgress() === undefined,
}} }}
> >
<Show when={decryptionProgress() !== undefined}> <Show when={decryptionProgress() !== undefined}>
@ -73,19 +93,62 @@ export const Home: Component<RouteSectionProps> = () => {
</div> </div>
</Progress> </Progress>
</Show> </Show>
<Show when={isLoadingDatabase()}> <Show when={loadingProgress() !== undefined}>
{/* <p class="font-bold text-2xl">Loading database</p>
<p class="text-muted-foreground">This can take some time</p> */}
<p class="font-bold text-2xl">Loading database</p> <p class="font-bold text-2xl">Loading database</p>
<Progress
value={loadingProgress()}
minValue={0}
maxValue={100}
getValueLabel={({ value }) => `${value}%`}
class="w-[300px] space-y-1"
>
<div class="flex justify-between">
<ProgressLabel>Loading...</ProgressLabel>
<ProgressValueLabel />
</div>
</Progress>
</Show> </Show>
</Flex> </Flex>
</Portal> </Portal>
<Title>Signal stats</Title> <Title>Signal stats</Title>
<div> <form class="flex flex-col gap-y-8 p-8" onSubmit={onSubmit}>
<input <TextField onChange={(value) => setPassphrase(value)}>
type="password" <TextFieldLabel>Passphrase</TextFieldLabel>
onChange={(event) => setPassphrase(event.currentTarget.value)} <TextFieldInput type="password" class="max-w-md" />
/> </TextField>
<input type="file" accept=".backup" onChange={onFileChange} /> <Flex
</div> ref={dropzone.setRef}
justifyContent="center"
alignItems="center"
class="relative min-h-32 min-w-96 max-w-xl rounded-lg border-4 border-border border-dashed"
classList={{
"border-ring": dropzone.isDragging(),
}}
>
<Button
onClick={() =>
fileUploader.selectFiles((files) => {
setBackupFile(files.at(0)?.file);
})
}
>
Select backup file
</Button>
<span
class="absolute bottom-2"
classList={{
"text-muted-foreground": !backupFile(),
}}
>
{backupFile() ? backupFile()?.name : "or drop the file here"}
</span>
</Flex>
<Button type="submit" class="max-w-72">
Decrypt and load backup
</Button>
</form>
</> </>
); );
}; };

View file

@ -1,23 +1,16 @@
import type { RouteSectionProps } from "@solidjs/router"; import type { RouteSectionProps } from "@solidjs/router";
import { type Component, createResource, Show } from "solid-js"; import { type Component, createResource, Show } from "solid-js";
import { import { allThreadsOverviewQuery, overallSentMessagesQuery, SELF_ID } from "~/db";
allThreadsOverviewQuery,
overallSentMessagesQuery,
} from "~/db-queries";
import { Title } from "@solidjs/meta"; import { Title } from "@solidjs/meta";
import { SELF_ID } from "~/db";
import { getNameFromRecipient } from "~/lib/get-name-from-recipient"; import { getNameFromRecipient } from "~/lib/get-name-from-recipient";
import { OverviewTable, type RoomOverview } from "./overview-table"; import { OverviewTable, type RoomOverview } from "./overview-table";
export const Overview: Component<RouteSectionProps> = () => { export const Overview: Component<RouteSectionProps> = () => {
const [allSelfSentMessagesCount] = createResource(() => const [allSelfSentMessagesCount] = createResource(() => overallSentMessagesQuery(SELF_ID));
overallSentMessagesQuery(SELF_ID),
);
const [roomOverview] = createResource<RoomOverview[] | undefined>( const [roomOverview] = createResource<RoomOverview[] | undefined>(async () => {
async () => {
return (await allThreadsOverviewQuery())?.map((row) => { return (await allThreadsOverviewQuery())?.map((row) => {
const isGroup = row.title !== null; const isGroup = row.title !== null;
@ -26,11 +19,7 @@ export const Overview: Component<RouteSectionProps> = () => {
if (row.title !== null) { if (row.title !== null) {
name = row.title; name = row.title;
} else { } else {
name = getNameFromRecipient( name = getNameFromRecipient(row.nickname_joined_name, row.system_joined_name, row.profile_joined_name);
row.nickname_joined_name,
row.system_joined_name,
row.profile_joined_name,
);
} }
return { return {
@ -38,30 +27,22 @@ export const Overview: Component<RouteSectionProps> = () => {
recipientId: row.recipient_id, recipientId: row.recipient_id,
archived: Boolean(row.archived), archived: Boolean(row.archived),
messageCount: row.message_count, messageCount: row.message_count,
lastMessageDate: row.last_message_date lastMessageDate: row.last_message_date ? new Date(row.last_message_date) : undefined,
? new Date(row.last_message_date)
: undefined,
name, name,
isGroup, isGroup,
}; };
}); });
}, });
);
return ( return (
<> <>
<Title>Signal statistics overview</Title> <Title>Signal statistics overview</Title>
<div> <div>
<p> <p>All messages: {allSelfSentMessagesCount()?.messageCount as number}</p>
All messages: {allSelfSentMessagesCount()?.messageCount as number} <Show when={!roomOverview.loading && roomOverview()} fallback="Loading...">
</p>
<Show
when={!roomOverview.loading && roomOverview()}
fallback="Loading..."
>
{(currentRoomOverview) => ( {(currentRoomOverview) => (
<OverviewTable data={currentRoomOverview()} /> console.log(currentRoomOverview()), (<OverviewTable data={currentRoomOverview()} />)
)} )}
</Show> </Show>
</div> </div>