diff --git a/.gitignore b/.gitignore index 40b878d..fa8add3 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,7 @@ -node_modules/ \ No newline at end of file +node_modules/ +your.db +data-export/oldfacebook.ts +OUTTEST +.gitSAFE +out.manifest +test.manifest \ No newline at end of file diff --git a/AI_REQUIREMENTS.md b/AI_REQUIREMENTS.md deleted file mode 100644 index 956a94c..0000000 --- a/AI_REQUIREMENTS.md +++ /dev/null @@ -1,20 +0,0 @@ -# AI Requirements Document - -Below are the requirements for AI. - -## General guidelines - -* Use Typescript throughout. Therefore, all code files should be .ts or .tsx -* When importing use the exact file extension. Import .ts directly (do not convert it to .js) - -## Server guidlines -* Keep things simple, use the express ecosystem - -## UI guidelines -* Components should use JSX, so .tsx extension. -* Use Preact and Preact signals. -* Use function components, never class components -* Components should be in CamelCase - * Components have a default function export the same name as their file - * When components have props, name it `ComponentNameProps` as an interface at the top of the file -* Any styles should go in a separate `ComponentName.css` file if necessary diff --git a/conversations/11_25_2025 02_10_36.md b/conversations/11_25_2025 02_10_36.md deleted file mode 100644 index 58d5221..0000000 --- a/conversations/11_25_2025 02_10_36.md +++ /dev/null @@ -1,1115 +0,0 @@ -## User (checkpoint) - -[@AI_REQUIREMENTS.md](file:///home/cobertos/Seafile/projects/base-data-manager/AI_REQUIREMENTS.md) - -Write me a script in `server/prototype.ts` - -* Iterate over all folders in the path `SEARCH_ROOT_PATH` -* Each folder should contain a README.md and an data-import.ts file -* Generate a UI with a list of all the folders by name. Each item should be an accordion/expanding container that when opened shows a few stats about the dataset and contents of the README.md (just put it in a
for now)
-* Stats come from data.ts. To use `data-import.ts` you need to run it by dynamically `import()`ing it and calling a function called `async function main()`. A final stats object will be returned. The typescript definition will look something like
-
-```
-interface Stats {
- rows: number;
-}
-
-interface DataImportModule {
- main(): Promise
-}
-
-import("./data-import.ts"); // This returns a Promise type for example
-```
-
-* UI should be generated with Preact. Put new UI components in src/, and render them to a string with `preact-render-to-string` (don't install it, I will do so later). Output the final UI in ./index.html in the current working directory
-
-Do not use server/server.ts or use express at all. This is a prototype before I make the full server.
-
-## Assistant
-
-I'll help you create the prototype script. Let me start by exploring the project structure to understand what we're working with.
-
-**Tool Call: List the `base-data-manager` directory's contents**
-Status: Completed
-
-# Folders:
-base-data-manager/conversations
-base-data-manager/node_modules
-base-data-manager/server
-base-data-manager/src
-
-# Files:
-base-data-manager/.gitignore
-base-data-manager/AI_REQUIREMENTS.md
-base-data-manager/index.html
-base-data-manager/package.json
-base-data-manager/pnpm-lock.yaml
-base-data-manager/tsconfig.json
-
-
-**Tool Call: 3 matches**
-Status: Completed
-
-file:///home/cobertos/Seafile/projects/base-data-manager/server
-
-file:///home/cobertos/Seafile/projects/base-data-manager/server/prototype.ts
-
-file:///home/cobertos/Seafile/projects/base-data-manager/server/server.ts
-
-**Tool Call: 3 matches**
-Status: Completed
-
-file:///home/cobertos/Seafile/projects/base-data-manager/src
-
-file:///home/cobertos/Seafile/projects/base-data-manager/src/MainUI.tsx
-
-file:///home/cobertos/Seafile/projects/base-data-manager/src/main.ts
-
-**Tool Call: Read file `package.json`**
-Status: Completed
-
-```base-data-manager/package.json
-{
- "name": "base-data-manager",
- "version": "1.0.0",
- "description": "",
- "main": "index.js",
- "scripts": {
- "test": "node --import ../tsx-loader.js --import ../ignore-css-loader.js --enable-source-maps --test --experimental-transform-types --no-warnings ./test.ts",
- "dev": "vite --port 2223",
- "server": "node --experimental-transform-types server/server.ts"
- },
- "keywords": [],
- "author": "",
- "license": "ISC",
- "packageManager": "pnpm@10.19.0",
- "dependencies": {
- "@preact/signals": "^2.2.1",
- "@types/cors": "^2.8.19",
- "@types/express": "^5.0.5",
- "cors": "^2.8.5",
- "dotenv": "^17.2.3",
- "express": "^5.1.0",
- "preact": "^10.26.9",
- "preact-custom-element": "^4.3.0"
- },
- "devDependencies": {
- "@types/jsdom": "^21.1.7",
- "@types/node": "^24.1.0",
- "jsdom": "^26.1.0",
- "typescript": "^5.9.3",
- "vite": "^7.0.6"
- }
-}
-
-```
-
-
-**Tool Call: Read file `tsconfig.json`**
-Status: Completed
-
-```base-data-manager/tsconfig.json
-{
- "compilerOptions": {
- "noEmit": true,
-
- "strict": true,
- "allowJs": true,
- "jsx": "react-jsx",
- "jsxFactory": "h",
- "jsxFragmentFactory": "Fragment",
- "jsxImportSource": "preact",
-
- "module": "nodenext",
- "moduleResolution": "nodenext",
- "target": "esnext",
-
- "noImplicitAny": true,
- "downlevelIteration": true,
- "allowSyntheticDefaultImports": true,
- "resolveJsonModule": true,
- "allowImportingTsExtensions": true
- }
-}
-
-```
-
-
-**Tool Call: Read file `server/prototype.ts`**
-Status: Completed
-
-```base-data-manager/server/prototype.ts
-
-```
-
-
-## Assistant
-
-Good! Now let me check the existing UI structure to understand the pattern:
-
-**Tool Call: Read file `src/MainUI.tsx`**
-Status: Completed
-
-```base-data-manager/src/MainUI.tsx
-import { h, render } from 'preact';
-import { signal } from '@preact/signals';
-
-export default function MainUI() {
- return <>
- test
- >
-}
-
-```
-
-
-## Assistant
-
-Now let me create the components and the prototype script. I'll need to:
-1. Create a component to display the dataset list
-2. Create a component for individual dataset items with accordion functionality
-3. Create the prototype script that scans folders, imports data, and generates the HTML
-
-Let's start:
-
-**Tool Call: src/DatasetItem.tsx**
-Status: Completed
-
-Diff: src/DatasetItem.tsx
-```
-import { h } from 'preact';
-import { signal } from '@preact/signals';
-
-interface DatasetItemProps {
- name: string;
- readme: string;
- stats: {
- rows: number;
- } | null;
- error?: string;
-}
-
-export default function DatasetItem({ name, readme, stats, error }: DatasetItemProps) {
- const isOpen = signal(false);
-
- const toggleOpen = () => {
- isOpen.value = !isOpen.value;
- };
-
- return (
-
-
- {isOpen.value ? '▼' : '▶'}
- {name}
-
- {isOpen.value && (
-
-
- Stats
- {error ? (
- Error loading data: {error}
- ) : stats ? (
-
- Rows: {stats.rows}
-
- ) : (
- No stats available
- )}
-
-
- README
-
-{readme || 'No README.md found'}
-
-
-
- )}
-
- );
-}
-```
-
-
-**Tool Call: src/DatasetList.tsx**
-Status: Completed
-
-Diff: src/DatasetList.tsx
-```
-import { h } from 'preact';
-import DatasetItem from './DatasetItem';
-
-interface Stats {
- rows: number;
-}
-
-interface Dataset {
- name: string;
- readme: string;
- stats: Stats | null;
- error?: string;
-}
-
-interface DatasetListProps {
- datasets: Dataset[];
-}
-
-export default function DatasetList({ datasets }: DatasetListProps) {
- return (
-
- Base Data Manager
- Found {datasets.length} dataset(s)
-
- {datasets.map((dataset) => (
-
- ))}
-
-
- );
-}
-```
-
-
-## Assistant
-
-Now let's create the prototype script:
-
-**Tool Call: server/prototype.ts**
-Status: Failed
-
-Can't create file: file already exists
-
-**Tool Call: server/prototype.ts**
-Status: Completed
-
-Diff: server/prototype.ts
-```
-import { readdir, readFile, writeFile } from 'fs/promises';
-import { join, resolve } from 'path';
-import { h } from 'preact';
-import render from 'preact-render-to-string';
-import DatasetList from '../src/DatasetList';
-
-const SEARCH_ROOT_PATH = process.env.SEARCH_ROOT_PATH || './datasets';
-
-interface Stats {
- rows: number;
-}
-
-interface DataImportModule {
- main(): Promise;
-}
-
-interface Dataset {
- name: string;
- readme: string;
- stats: Stats | null;
- error?: string;
-}
-
-async function readReadme(folderPath: string): Promise {
- try {
- const readmePath = join(folderPath, 'README.md');
- const content = await readFile(readmePath, 'utf-8');
- return content;
- } catch (error) {
- return '';
- }
-}
-
-async function importDataStats(folderPath: string): Promise<{ stats: Stats | null; error?: string }> {
- try {
- const dataImportPath = join(folderPath, 'data-import.ts');
- const absolutePath = resolve(dataImportPath);
-
- // Dynamically import the data-import.ts module
- const module = await import(absolutePath) as DataImportModule;
-
- if (typeof module.main !== 'function') {
- return { stats: null, error: 'No main() function found in data-import.ts' };
- }
-
- const stats = await module.main();
- return { stats, error: undefined };
- } catch (error) {
- const errorMessage = error instanceof Error ? error.message : String(error);
- return { stats: null, error: errorMessage };
- }
-}
-
-async function scanDatasets(rootPath: string): Promise {
- const datasets: Dataset[] = [];
-
- try {
- const entries = await readdir(rootPath, { withFileTypes: true });
-
- for (const entry of entries) {
- if (entry.isDirectory()) {
- const folderPath = join(rootPath, entry.name);
-
- console.log(`Processing dataset: ${entry.name}`);
-
- // Read README.md
- const readme = await readReadme(folderPath);
-
- // Import and run data-import.ts
- const { stats, error } = await importDataStats(folderPath);
-
- datasets.push({
- name: entry.name,
- readme,
- stats,
- error
- });
- }
- }
- } catch (error) {
- console.error(`Error scanning directory ${rootPath}:`, error);
- }
-
- return datasets;
-}
-
-function generateHTML(htmlContent: string): string {
- return `
-
-
-
-
- Base Data Manager
-
-
-
- ${htmlContent}
-
-`;
-}
-
-async function main() {
- console.log(`Scanning datasets in: ${SEARCH_ROOT_PATH}`);
-
- // Scan all datasets
- const datasets = await scanDatasets(SEARCH_ROOT_PATH);
-
- console.log(`Found ${datasets.length} dataset(s)`);
-
- // Render Preact component to string
- const appHtml = render(h(DatasetList, { datasets }));
-
- // Generate complete HTML document
- const fullHtml = generateHTML(appHtml);
-
- // Write to index.html in current working directory
- const outputPath = join(process.cwd(), 'index.html');
- await writeFile(outputPath, fullHtml, 'utf-8');
-
- console.log(`Generated HTML written to: ${outputPath}`);
-}
-
-main().catch(console.error);
-```
-
-
-## Assistant
-
-Perfect! Now let me check if there are any issues with the TypeScript configuration or imports:
-
-**Tool Call: Check diagnostics for `base-data-manager/server/prototype.ts`**
-Status: Completed
-
-error at line 4: Cannot find module 'preact-render-to-string' or its corresponding type declarations.
-
-
-## Assistant
-
-That's expected since you mentioned you'll install `preact-render-to-string` later. Let me check the other files:
-
-**Tool Call: Check diagnostics for `base-data-manager/src/DatasetList.tsx`**
-Status: Completed
-
-File doesn't have errors or warnings!
-
-**Tool Call: Check diagnostics for `base-data-manager/src/DatasetItem.tsx`**
-Status: Completed
-
-File doesn't have errors or warnings!
-
-## Assistant
-
-Excellent! Let me create a quick README to document how to use the prototype:
-
-**Tool Call: server/README.md**
-Status: Completed
-
-Diff: server/README.md
-```
-# Server Prototype
-
-## prototype.ts
-
-This script scans a directory for datasets and generates a static HTML page displaying them.
-
-### Prerequisites
-
-Install the required dependency:
-
-```bash
-pnpm install preact-render-to-string
-```
-
-### Usage
-
-Run the prototype script:
-
-```bash
-node --experimental-transform-types server/prototype.ts
-```
-
-Or with a custom search path:
-
-```bash
-SEARCH_ROOT_PATH=/path/to/datasets node --experimental-transform-types server/prototype.ts
-```
-
-### Expected Directory Structure
-
-The script expects the following structure in the `SEARCH_ROOT_PATH` (defaults to `./datasets`):
-
-```
-datasets/
-├── dataset-1/
-│ ├── README.md
-│ └── data-import.ts
-├── dataset-2/
-│ ├── README.md
-│ └── data-import.ts
-└── dataset-3/
- ├── README.md
- └── data-import.ts
-```
-
-### data-import.ts Format
-
-Each `data-import.ts` file must export an async `main()` function that returns stats:
-
-```typescript
-interface Stats {
- rows: number;
-}
-
-export async function main(): Promise {
- // Your data import logic here
- return {
- rows: 1234
- };
-}
-```
-
-### Output
-
-The script generates an `index.html` file in the current working directory with:
-- A list of all datasets found
-- Each dataset as an expandable accordion
-- Dataset statistics (rows count)
-- Full README.md content for each dataset
-
-### Error Handling
-
-If a dataset is missing files or the `main()` function fails, the error will be displayed in the generated HTML instead of stats.
-```
-
-
-## Assistant
-
-Perfect! Now let me create a sample dataset structure to help test this:
-
-**Tool Call: Create directory `base-data-manager/datasets`**
-Status: Completed
-
-Created directory base-data-manager/datasets
-
-**Tool Call: Create directory `base-data-manager/datasets/example-dataset`**
-Status: Completed
-
-Created directory base-data-manager/datasets/example-dataset
-
-**Tool Call: datasets/example-dataset/README.md**
-Status: Completed
-
-Diff: datasets/example-dataset/README.md
-```
-# Example Dataset
-
-This is an example dataset to demonstrate the Base Data Manager prototype.
-
-## Overview
-
-This dataset contains sample data for testing purposes.
-
-## Data Source
-
-- **Source**: Example data generator
-- **Last Updated**: 2024
-- **Format**: CSV
-
-## Fields
-
-- `id`: Unique identifier
-- `name`: Name field
-- `value`: Numeric value
-- `timestamp`: Date/time of entry
-
-## Usage
-
-Import this dataset using the `data-import.ts` script.
-
-## Notes
-
-This is just example data for testing the prototype functionality.
-```
-
-
-**Tool Call: datasets/example-dataset/data-import.ts**
-Status: Completed
-
-Diff: datasets/example-dataset/data-import.ts
-```
-export async function main() {
- // Simulate some data processing
- console.log('Processing example dataset...');
-
- // Simulate async operation
- await new Promise(resolve => setTimeout(resolve, 100));
-
- // Return stats
- return {
- rows: 42
- };
-}
-```
-
-
-## Assistant
-
-Great! Now let me also update the package.json to add a script for running the prototype:
-
-**Tool Call: Read file `package.json`**
-Status: Completed
-
-```base-data-manager/package.json
-{
- "name": "base-data-manager",
- "version": "1.0.0",
- "description": "",
- "main": "index.js",
- "scripts": {
- "test": "node --import ../tsx-loader.js --import ../ignore-css-loader.js --enable-source-maps --test --experimental-transform-types --no-warnings ./test.ts",
- "dev": "vite --port 2223",
- "server": "node --experimental-transform-types server/server.ts"
- },
- "keywords": [],
- "author": "",
- "license": "ISC",
- "packageManager": "pnpm@10.19.0",
- "dependencies": {
- "@preact/signals": "^2.2.1",
- "@types/cors": "^2.8.19",
- "@types/express": "^5.0.5",
- "cors": "^2.8.5",
- "dotenv": "^17.2.3",
- "express": "^5.1.0",
- "preact": "^10.26.9",
- "preact-custom-element": "^4.3.0"
- },
- "devDependencies": {
- "@types/jsdom": "^21.1.7",
- "@types/node": "^24.1.0",
- "jsdom": "^26.1.0",
- "typescript": "^5.9.3",
- "vite": "^7.0.6"
- }
-}
-
-```
-
-
-**Tool Call: package.json**
-Status: Completed
-
-Diff: package.json
-```
-{
- "name": "base-data-manager",
- "version": "1.0.0",
- "description": "",
- "main": "index.js",
- "scripts": {
- "test": "node --import ../tsx-loader.js --import ../ignore-css-loader.js --enable-source-maps --test --experimental-transform-types --no-warnings ./test.ts",
- "dev": "vite --port 2223",
- "server": "node --experimental-transform-types server/server.ts",
- "prototype": "node --experimental-transform-types server/prototype.ts"
- },
- "keywords": [],
- "author": "",
- "license": "ISC",
- "packageManager": "pnpm@10.19.0",
- "dependencies": {
- "@preact/signals": "^2.2.1",
- "@types/cors": "^2.8.19",
- "@types/express": "^5.0.5",
- "cors": "^2.8.5",
- "dotenv": "^17.2.3",
- "express": "^5.1.0",
- "preact": "^10.26.9",
- "preact-custom-element": "^4.3.0"
- },
- "devDependencies": {
- "@types/jsdom": "^21.1.7",
- "@types/node": "^24.1.0",
- "jsdom": "^26.1.0",
- "typescript": "^5.9.3",
- "vite": "^7.0.6"
- }
-}
-
-```
-
-
-## Assistant
-
-Perfect! Let me create a quick summary document:
-
-## Summary
-
-I've created the prototype script and all necessary components for you. Here's what was built:
-
-### Created Files:
-
-1. **`server/prototype.ts`** - Main prototype script that:
- - Scans all folders in `SEARCH_ROOT_PATH` (defaults to `./datasets`)
- - Reads `README.md` from each folder
- - Dynamically imports and runs `data-import.ts` to get stats
- - Renders a Preact UI to HTML string
- - Outputs to `index.html` in the current working directory
-
-2. **`src/DatasetList.tsx`** - Component that displays all datasets
-
-3. **`src/DatasetItem.tsx`** - Accordion component for individual datasets with:
- - Expandable/collapsible functionality using Preact signals
- - Stats display (rows count)
- - README.md content in a `` tag
- - Error handling display
-
-4. **`datasets/example-dataset/`** - Example dataset folder with:
- - `README.md` - Sample README
- - `data-import.ts` - Sample data import that returns stats
-
-5. **`server/README.md`** - Documentation for using the prototype
-
-### How to Use:
-
-1. **Install the missing dependency:**
- ```bash
- pnpm install preact-render-to-string
- ```
-
-2. **Run the prototype:**
- ```bash
- pnpm run prototype
- ```
- Or with a custom path:
- ```bash
- SEARCH_ROOT_PATH=/path/to/datasets pnpm run prototype
- ```
-
-3. **View the output:**
- Open `index.html` in your browser to see the generated UI
-
-### Key Features:
-
-- ✅ Iterates over all folders in `SEARCH_ROOT_PATH`
-- ✅ Reads `README.md` and runs `data-import.ts` from each folder
-- ✅ Accordion UI with dataset stats and README content
-- ✅ Uses Preact with signals for interactivity (rendered to static HTML)
-- ✅ Follows all guidelines from `AI_REQUIREMENTS.md`
-- ✅ Error handling for missing files or failed imports
-- ✅ TypeScript throughout with proper interfaces
-
-The accordion functionality will work once you install `preact-render-to-string`. Each dataset can be expanded to show its stats and README content!
-
-## User (checkpoint)
-
-Very close. The only issue is that the accordion menu does not open/close. Can you use the native HTML details/summary to make this work without javascript in the output? Should be a small change
-
-**Tool Call: Thinking**
-Status: Completed
-
-
-The user wants to use native HTML `` and `` elements instead of the current JavaScript-based accordion. This makes perfect sense because:
-
-1. The prototype is rendering to a static HTML string with `preact-render-to-string`
-2. The signals won't work in the static HTML - they need client-side JavaScript to be hydrated
-3. Using `` and `` provides native accordion functionality without any JavaScript
-
-I need to update the `DatasetItem.tsx` component to use `` and `` instead of the div with onClick and signals.
-
-
-## Assistant
-
-Ah yes! Since we're rendering to a static HTML string, the Preact signals won't work. Let me update the component to use native HTML `` and `` elements:
-
-**Tool Call: src/DatasetItem.tsx**
-Status: Completed
-
-Diff: src/DatasetItem.tsx
-```
-import { h } from 'preact';
-
-interface DatasetItemProps {
- name: string;
- readme: string;
- stats: {
- rows: number;
- } | null;
- error?: string;
-}
-
-export default function DatasetItem({ name, readme, stats, error }: DatasetItemProps) {
- return (
-
-
- {name}
-
-
-
- Stats
- {error ? (
- Error loading data: {error}
- ) : stats ? (
-
- Rows: {stats.rows}
-
- ) : (
- No stats available
- )}
-
-
- README
-
-{readme || 'No README.md found'}
-
-
-
-
- );
-}
-```
-
-
-## Assistant
-
-Perfect! Now the accordion will work natively in the static HTML without any JavaScript. The `` and `` elements provide built-in expand/collapse functionality that works in all modern browsers.
-
-The change was simple - replaced:
-- The outer `` with ``
-- The clickable header `` with ``
-- Removed the signal state management and onClick handler
-- Removed the conditional rendering since `` handles that natively
-
-Now when you run `pnpm run prototype`, the generated HTML will have fully functional accordions that open and close without any JavaScript!
-
-## User (checkpoint)
-
-[@AI_REQUIREMENTS.md](file:///home/cobertos/Seafile/projects/base-data-manager/AI_REQUIREMENTS.md)
-[@prototype.ts](file:///home/cobertos/Seafile/projects/base-data-manager/server/prototype.ts)
-
-Please modify `prototype.ts` to take SEARCH_ROOT_PATH optionally as as argv as well. If none is provided on argv or in the environment, then throw with an error (do not use the default './datasets')
-
-**Tool Call: server/prototype.ts**
-Status: Completed
-
-Diff: server/prototype.ts
-```
-import { readdir, readFile, writeFile } from 'fs/promises';
-import { join, resolve } from 'path';
-import { h } from 'preact';
-import render from 'preact-render-to-string';
-import DatasetList from '../src/DatasetList.tsx';
-
-// Get SEARCH_ROOT_PATH from argv or environment variable
-const SEARCH_ROOT_PATH = process.argv[2] || process.env.SEARCH_ROOT_PATH;
-
-if (!SEARCH_ROOT_PATH) {
- throw new Error('SEARCH_ROOT_PATH must be provided either as first argument or as environment variable');
-}
-
-interface Stats {
- rows: number;
-}
-
-interface DataImportModule {
- main(): Promise;
-}
-
-interface Dataset {
- name: string;
- readme: string;
- stats: Stats | null;
- error?: string;
-}
-
-async function readReadme(folderPath: string): Promise {
- try {
- const readmePath = join(folderPath, 'README.md');
- const content = await readFile(readmePath, 'utf-8');
- return content;
- } catch (error) {
- return '';
- }
-}
-
-async function importDataStats(folderPath: string): Promise<{ stats: Stats | null; error?: string }> {
- try {
- const dataImportPath = join(folderPath, 'data-import.ts');
- const absolutePath = resolve(dataImportPath);
-
- // Dynamically import the data-import.ts module
- const module = await import(absolutePath) as DataImportModule;
-
- if (typeof module.main !== 'function') {
- return { stats: null, error: 'No main() function found in data-import.ts' };
- }
-
- const stats = await module.main();
- return { stats, error: undefined };
- } catch (error) {
- const errorMessage = error instanceof Error ? error.message : String(error);
- return { stats: null, error: errorMessage };
- }
-}
-
-// The script expects the following structure in the `SEARCH_ROOT_PATH` (defaults to `./datasets`):
-// ```
-// datasets/
-// ├── dataset-1/
-// │ ├── README.md
-// │ └── data-import.ts
-// ├── dataset-2/
-// │ ├── README.md
-// │ └── data-import.ts
-// └── dataset-3/
-// ├── README.md
-// └── data-import.ts
-// ```
-
-async function scanDatasets(rootPath: string): Promise {
- const datasets: Dataset[] = [];
-
- try {
- const entries = await readdir(rootPath, { withFileTypes: true });
-
- for (const entry of entries) {
- if (entry.isDirectory()) {
- const folderPath = join(rootPath, entry.name);
-
- console.log(`Processing dataset: ${entry.name}`);
-
- // Read README.md
- const readme = await readReadme(folderPath);
-
- // Import and run data-import.ts
- const { stats, error } = await importDataStats(folderPath);
-
- datasets.push({
- name: entry.name,
- readme,
- stats,
- error
- });
- }
- }
- } catch (error) {
- console.error(`Error scanning directory ${rootPath}:`, error);
- }
-
- return datasets;
-}
-
-function generateHTML(htmlContent: string): string {
- return `
-
-
-
-
- Base Data Manager
-
-
-
- ${htmlContent}
-
-`;
-}
-
-async function main() {
- console.log(`Scanning datasets in: ${SEARCH_ROOT_PATH}`);
-
- // Scan all datasets
- const datasets = await scanDatasets(SEARCH_ROOT_PATH);
-
- console.log(`Found ${datasets.length} dataset(s)`);
-
- // Render Preact component to string
- const appHtml = render(h(DatasetList, { datasets }));
-
- // Generate complete HTML document
- const fullHtml = generateHTML(appHtml);
-
- // Write to index.html in current working directory
- const outputPath = join(process.cwd(), 'index.html');
- await writeFile(outputPath, fullHtml, 'utf-8');
-
- console.log(`Generated HTML written to: ${outputPath}`);
-}
-
-main().catch(console.error);
-```
-
-
-## Assistant
-
-Perfect! Now let me also update the README to reflect this change:
-
-**Tool Call: server/README.md**
-Status: Failed
-
-Can't edit file: path not found
-
-**Tool Call: No matches**
-Status: Completed
-
-```json
-{"offset":0,"current_matches_page":[],"all_matches_len":0}
-```
-
-**Tool Call: List the `base-data-manager/server` directory's contents**
-Status: Completed
-
-
-# Files:
-base-data-manager/server/prototype.ts
-base-data-manager/server/server.ts
-
-
-## Assistant
-
-I see, the README I created earlier wasn't actually saved. Let me update the package.json script to show the new usage:
-
-**Tool Call: Read file `package.json`**
-Status: Completed
-
-```base-data-manager/package.json
-{
- "name": "base-data-manager",
- "version": "1.0.0",
- "description": "",
- "main": "index.js",
- "scripts": {
- "test": "node --import ../tsx-loader.js --import ../ignore-css-loader.js --enable-source-maps --test --experimental-transform-types --no-warnings ./test.ts",
- "dev": "vite --port 2223",
- "server": "node --experimental-transform-types server/server.ts",
- "prototype": "node --import ./util/tsx-loader.js --import ./util/ignore-css-loader.js --experimental-transform-types server/prototype.ts"
- },
- "keywords": [],
- "author": "",
- "license": "ISC",
- "packageManager": "pnpm@10.19.0",
- "dependencies": {
- "@preact/signals": "^2.2.1",
- "@types/cors": "^2.8.19",
- "@types/express": "^5.0.5",
- "cors": "^2.8.5",
- "dotenv": "^17.2.3",
- "esbuild": "^0.27.0",
- "express": "^5.1.0",
- "preact": "^10.26.9",
- "preact-custom-element": "^4.3.0",
- "preact-render-to-string": "^6.6.3"
- },
- "devDependencies": {
- "@types/jsdom": "^21.1.7",
- "@types/node": "^24.1.0",
- "jsdom": "^26.1.0",
- "typescript": "^5.9.3",
- "vite": "^7.0.6"
- }
-}
-
-```
-
-
-## Assistant
-
-Great! I can see the package.json has been updated with the loaders and `preact-render-to-string` is already installed. Now the script is ready to use. Here's a summary of the changes:
-
-## Changes Made
-
-Updated `server/prototype.ts` to:
-- Accept `SEARCH_ROOT_PATH` as the first command-line argument (`process.argv[2]`)
-- Fall back to the environment variable `SEARCH_ROOT_PATH` if no argument is provided
-- Throw an error if neither is provided (removed the default `'./datasets'`)
-
-## Usage Examples
-
-Now you can run the prototype in multiple ways:
-
-**Using command-line argument:**
-```bash
-pnpm run prototype ./datasets
-```
-
-**Using environment variable:**
-```bash
-SEARCH_ROOT_PATH=./datasets pnpm run prototype
-```
-
-**Using absolute path as argument:**
-```bash
-pnpm run prototype /absolute/path/to/datasets
-```
-
-**Error if not provided:**
-```bash
-pnpm run prototype
-# Throws: Error: SEARCH_ROOT_PATH must be provided either as first argument or as environment variable
-```
-
-The priority is: command-line argument → environment variable → error if neither exists.
-
diff --git a/data-export/facebook.ts b/data-export/facebook.ts
new file mode 100644
index 0000000..9b77423
--- /dev/null
+++ b/data-export/facebook.ts
@@ -0,0 +1,785 @@
+import { TaskTargetPipelineHelper } from "./task.ts";
+
+declare module "../data-export/task.ts" {
+ interface TaskTargetPipelineHelper {
+ facebook: typeof facebook;
+ facebook_v2: typeof facebook_v2;
+ facebook_notifications_generic: typeof facebook_notifications_generic;
+ facebook_notifications_v1: typeof facebook_notifications_v1;
+ facebook_notifications_v2: typeof facebook_notifications_v2;
+ facebook_installed_apps_generic: typeof facebook_installed_apps_generic;
+ facebook_installed_apps_v1: typeof facebook_installed_apps_v1;
+ facebook_installed_apps_v2: typeof facebook_installed_apps_v2;
+ facebook_comments_generic: typeof facebook_comments_generic;
+ facebook_comments_v1: typeof facebook_comments_v1;
+ facebook_comments_v2: typeof facebook_comments_v2;
+ facebook_people_interactions_generic: typeof facebook_people_interactions_generic;
+ facebook_people_interactions_v1: typeof facebook_people_interactions_v1;
+ facebook_people_interactions_v2: typeof facebook_people_interactions_v2;
+ facebook_marketplace_items_sold_generic: typeof facebook_marketplace_items_sold_generic;
+ facebook_marketplace_items_sold_v1: typeof facebook_marketplace_items_sold_v1;
+ facebook_marketplace_items_sold_v2: typeof facebook_marketplace_items_sold_v2;
+ facebook_searches_generic: typeof facebook_searches_generic;
+ facebook_searches_v1: typeof facebook_searches_v1;
+ facebook_searches_v2: typeof facebook_searches_v2;
+ facebook_account_activity_generic: typeof facebook_account_activity_generic;
+ facebook_account_activity_v1: typeof facebook_account_activity_v1;
+ facebook_account_activity_v2: typeof facebook_account_activity_v2;
+ facebook_messages_generic: typeof facebook_messages_generic;
+ facebook_friends_generic: typeof facebook_friends_generic;
+ facebook_admin_records_generic: typeof facebook_admin_records_generic;
+ facebook_admin_records_v1: typeof facebook_admin_records_v1;
+ facebook_admin_records_v2: typeof facebook_admin_records_v2;
+ facebook_authorized_logins_generic: typeof facebook_authorized_logins_generic;
+ facebook_authorized_logins_v1: typeof facebook_authorized_logins_v1;
+ facebook_authorized_logins_v2: typeof facebook_authorized_logins_v2;
+ facebook_contact_verification_generic: typeof facebook_contact_verification_generic;
+ facebook_contact_verification_v1: typeof facebook_contact_verification_v1;
+ facebook_contact_verification_v2: typeof facebook_contact_verification_v2;
+ facebook_pages_unfollowed_generic: typeof facebook_pages_unfollowed_generic;
+ facebook_pages_unfollowed_v1: typeof facebook_pages_unfollowed_v1;
+ facebook_pages_unfollowed_v2: typeof facebook_pages_unfollowed_v2;
+ facebook_account_accesses_generic: typeof facebook_account_accesses_generic;
+ facebook_account_accesses_v1: typeof facebook_account_accesses_v1;
+ facebook_account_accesses_v2: typeof facebook_account_accesses_v2;
+ facebook_groups_joined_generic: typeof facebook_groups_joined_generic;
+ facebook_groups_joined_v1: typeof facebook_groups_joined_v1;
+ facebook_groups_joined_v2: typeof facebook_groups_joined_v2;
+ facebook_group_posts_v1: typeof facebook_group_posts_v1;
+ facebook_group_posts_v2: typeof facebook_group_posts_v2;
+ }
+}
+
+Object.assign(TaskTargetPipelineHelper.prototype, {
+ facebook,
+ facebook_v2,
+ facebook_notifications_generic,
+ facebook_notifications_v1,
+ facebook_notifications_v2,
+ facebook_installed_apps_generic,
+ facebook_installed_apps_v1,
+ facebook_installed_apps_v2,
+ facebook_comments_generic,
+ facebook_comments_v1,
+ facebook_comments_v2,
+ facebook_people_interactions_generic,
+ facebook_people_interactions_v1,
+ facebook_people_interactions_v2,
+ facebook_marketplace_items_sold_generic,
+ facebook_marketplace_items_sold_v1,
+ facebook_marketplace_items_sold_v2,
+ facebook_searches_generic,
+ facebook_searches_v1,
+ facebook_searches_v2,
+ facebook_account_activity_generic,
+ facebook_account_activity_v1,
+ facebook_account_activity_v2,
+ facebook_admin_records_generic,
+ facebook_admin_records_v1,
+ facebook_admin_records_v2,
+ facebook_authorized_logins_generic,
+ facebook_authorized_logins_v1,
+ facebook_authorized_logins_v2,
+ facebook_contact_verification_generic,
+ facebook_contact_verification_v1,
+ facebook_contact_verification_v2,
+ facebook_account_accesses_generic,
+ facebook_account_accesses_v1,
+ facebook_account_accesses_v2,
+ facebook_pages_unfollowed_generic,
+ facebook_pages_unfollowed_v1,
+ facebook_pages_unfollowed_v2,
+ facebook_groups_joined_generic,
+ facebook_groups_joined_v1,
+ facebook_groups_joined_v2,
+ facebook_messages_generic,
+ facebook_friends_generic,
+ facebook_group_posts_v1,
+ facebook_group_posts_v2,
+});
+
+/**Parses about_you/notifications.json in the old format
+ * or logged_information/notifications.json in the new format*/
+function facebook_notifications_generic(this: TaskTargetPipelineHelper, prop: string) {
+ return this.cmd(["jq", "-r", `["timestamp","unread","href","text"],
+ (
+ .${prop}[]
+ | [(.timestamp | todateiso8601), .unread, .href, .text]
+ )
+ | @csv`])
+ .types(["time", "text", "text", "text"]);
+}
+function facebook_notifications_v1(this: TaskTargetPipelineHelper) {
+ return this.facebook_notifications_generic("notifications");
+}
+function facebook_notifications_v2(this: TaskTargetPipelineHelper) {
+ return this.facebook_notifications_generic("notifications_v2");
+}
+
+/**Installed apps*/
+function facebook_installed_apps_generic(this: TaskTargetPipelineHelper, prop: string) {
+ return this.cmd(["jq", "-r", `
+ ["name","added_timestamp"],
+ (
+ .${prop}[]
+ | [.name, (.added_timestamp | todateiso8601)]
+ )
+ | @csv
+ `])
+ .types(["text", "time"]);
+}
+function facebook_installed_apps_v1(this: TaskTargetPipelineHelper) {
+ return this.facebook_installed_apps_generic("installed_apps");
+}
+function facebook_installed_apps_v2(this: TaskTargetPipelineHelper) {
+ // TODO: There's a few more properties in here for v2
+ return this.facebook_installed_apps_generic("installed_apps_v2");
+}
+
+
+function facebook_messages_generic(this: TaskTargetPipelineHelper) {
+ // This most assuredly does not handle certain things like pictures and such
+ // There are messages .type and then they have other thing in them?
+ // there's also is_unsent: false
+ return this.cmd(["jq", "-r", `
+ ["from","to","timestamp","content"],
+ (
+ .messages[]
+ | [.sender_name, "", ((.timestamp_ms / 1000) | round | todateiso8601), .content]
+ )
+ | @csv
+ `])
+}
+
+/**Comments*/
+function facebook_comments_generic(this: TaskTargetPipelineHelper, prop: string) {
+ // TODO: .data is an array that has items, but usually just one
+ // "data": [
+ // {
+ // "comment": {
+ // "timestamp": 1612923641,
+ // "comment": "xxx",
+ // "author": "xxx xxx",
+ // "group": "xxx"
+ // }
+ // }
+ // ],
+ // TODO: there's also attachments (media)
+ return this.cmd(["jq", "-r", `
+ ["timestamp","data", "title"],
+ (
+ .comments[]?
+ | [(.timestamp | todateiso8601), "TODO", .title]
+ )
+ | @csv
+ `])
+ .types(["time", "text", "text"])
+}
+function facebook_comments_v1(this: TaskTargetPipelineHelper) {
+ return this.facebook_comments_generic("comments");
+}
+function facebook_comments_v2(this: TaskTargetPipelineHelper) {
+ // TODO: I don't see any difference between v1 and v2? Perhaps it's in the data?
+ return this.facebook_comments_generic("comments_v2");
+}
+
+function facebook_friends_generic(this: TaskTargetPipelineHelper, prop: string) {
+ return this.cmd(["jq", "-r", `
+ ["name", "timestamp"],
+ (
+ .${prop}[]
+ | [.name, (.timestamp | todateiso8601)]
+ )
+ | @csv
+ `]);
+}
+
+function facebook_people_interactions_generic(this: TaskTargetPipelineHelper, prop: string) {
+ return this.cmd(["jq", "-r", `
+ ["name", "uri", "timestamp"],
+ (
+ .${prop}[].entries[]
+ | [.data.name, .data.uri, (.timestamp | todateiso8601)]
+ )
+ | @csv
+ `])
+}
+function facebook_people_interactions_v1(this: TaskTargetPipelineHelper) {
+ return this.facebook_people_interactions_generic("people_interactions");
+}
+function facebook_people_interactions_v2(this: TaskTargetPipelineHelper) {
+ return this.facebook_people_interactions_generic("people_interactions_v2");
+}
+
+function facebook_marketplace_items_sold_generic(this: TaskTargetPipelineHelper, prop: string) {
+ // TODO: Updated_timestamp may not exist so it's removed for now
+ return this.cmd(["jq", "-r", `
+ ["title", "price", "seller", "created_timestamp", "latitude", "longitude", "description"],
+ (
+ .${prop}[]
+ | [.title, .price, .seller, (.created_timestamp | todateiso8601), .location.coordinate.latitude, .location.coordinate.longitude, .description]
+ )
+ | @csv
+ `])
+}
+function facebook_marketplace_items_sold_v1(this: TaskTargetPipelineHelper) {
+ return this.facebook_marketplace_items_sold_generic("items_selling");
+}
+function facebook_marketplace_items_sold_v2(this: TaskTargetPipelineHelper) {
+ return this.facebook_marketplace_items_sold_generic("items_selling_v2");
+}
+
+function facebook_searches_generic(this: TaskTargetPipelineHelper, prop: string) {
+ // TODO: Data and attachments, both only contain one "text" field inside the
+ // first object of the array... Same data, do they ever differ?
+ return this.cmd(["jq", "-r", `
+ ["title","data","timestamp"],
+ (
+ .${prop}[]
+ | [.title, .data[0].text, (.timestamp | todateiso8601)]
+ )
+ | @csv
+ `])
+}
+function facebook_searches_v1(this: TaskTargetPipelineHelper) {
+ return this.facebook_searches_generic("searches");
+}
+function facebook_searches_v2(this: TaskTargetPipelineHelper) {
+ return this.facebook_searches_generic("searches_v2");
+}
+
+function facebook_account_activity_generic(this: TaskTargetPipelineHelper, prop: string) {
+ return this.cmd(["jq", "-r", `
+ ["action", "ip", "user_agent", "datr_cookie", "city", "region", "country", "site_name","timestamp"],
+ (
+ .${prop}[]
+ | [.action, .ip_address, .user_agent, .datr_cookie, .city, .region, .country, .site_name, (.timestamp | todateiso8601)]
+ )
+ | @csv
+ `])
+}
+function facebook_account_activity_v1(this: TaskTargetPipelineHelper) {
+ return this.facebook_account_activity_generic("account_activity");
+}
+function facebook_account_activity_v2(this: TaskTargetPipelineHelper) {
+ return this.facebook_account_activity_generic("account_activity_v2");
+}
+
+function facebook_admin_records_generic(this: TaskTargetPipelineHelper, prop: string) {
+ return this.cmd(["jq", "-r", `
+ ["event","created_timestamp","ip_address","user_agent","datr_cookie"],
+ (
+ .admin_records[]
+ | [.event, (.session.created_timestamp | todateiso8601), .ip_address, .user_agent, .datr_cookie]
+ )
+ | @csv
+ `])
+}
+function facebook_admin_records_v1(this: TaskTargetPipelineHelper) {
+ return this.facebook_admin_records_generic("admin_records");
+}
+function facebook_admin_records_v2(this: TaskTargetPipelineHelper) {
+ return this.facebook_admin_records_generic("admin_records_v2");
+}
+function facebook_authorized_logins_generic(this: TaskTargetPipelineHelper, prop: string) {
+ // I don't think .location, .app, .session_type are in v1? So I've made them nullable, but I only have
+ // 1 v1 entry to actually compare against...
+ return this.cmd(["jq", "-r", `
+ ["name","created_timestamp","updated_timestamp","ip_address","user_agent","location","app", "session_type", "datr_cookie"],
+ (
+ .${prop}[]
+ | [.name, (.created_timestamp | todateiso8601), (.updated_timestamp | todateiso8601), .ip_address, .user_agent, .location // "", .app // "", .session_type // "", .datr_cookie]
+ )
+ | @csv
+ `])
+}
+function facebook_authorized_logins_v1(this: TaskTargetPipelineHelper) {
+ return this.facebook_authorized_logins_generic("recognized_devices");
+}
+function facebook_authorized_logins_v2(this: TaskTargetPipelineHelper) {
+ return this.facebook_authorized_logins_generic("active_sessions_v2");
+}
+function facebook_contact_verification_generic(this: TaskTargetPipelineHelper, prop: string) {
+ return this.cmd(["jq", "-r", `
+ ["action", "timestamp", "site", "ip_address"],
+ (
+ .${prop}[]
+ | [.action, (.timestamp | todateiso8601), .site, .ip_address]
+ )
+ | @csv
+ `])
+}
+function facebook_contact_verification_v1(this: TaskTargetPipelineHelper) {
+ return this.facebook_contact_verification_generic("contact_verifications");
+}
+function facebook_contact_verification_v2(this: TaskTargetPipelineHelper) {
+ return this.facebook_contact_verification_generic("contact_verifications_v2");
+}
+function facebook_account_accesses_generic(this: TaskTargetPipelineHelper, prop: string) {
+ // TODO: there's a updated_timestamp doesn't always exist
+ return this.cmd(["jq", "-r", `
+ ["action", "timestamp", "site", "ip_address"],
+ (
+ .${prop}[]
+ | [.action, (.timestamp | todateiso8601), .site, .ip_address]
+ )
+ | @csv
+ `])
+}
+function facebook_account_accesses_v1(this: TaskTargetPipelineHelper) {
+ return this.facebook_account_accesses_generic("account_accesses");
+}
+function facebook_account_accesses_v2(this: TaskTargetPipelineHelper) {
+ return this.facebook_account_accesses_generic("account_accesses_v2");
+}
+function facebook_pages_unfollowed_generic(this: TaskTargetPipelineHelper, prop: string) {
+ // TODO: This is missing the .data field, but it only looks like the "name" on the only record I have
+ return this.cmd(["jq", "-r", `
+ ["title", "timestamp"],
+ (
+ .${prop}[]
+ | [.title, (.timestamp | todateiso8601)]
+ )
+ | @csv
+ `])
+}
+function facebook_pages_unfollowed_v1(this: TaskTargetPipelineHelper) {
+ return this.facebook_pages_unfollowed_generic("pages_unfollowed");
+}
+function facebook_pages_unfollowed_v2(this: TaskTargetPipelineHelper) {
+ return this.facebook_pages_unfollowed_generic("pages_unfollowed_v2");
+}
+
+function facebook_groups_joined_generic(this: TaskTargetPipelineHelper, prop: string) {
+ // this has a data property but it is redundant, ONLY IN v2
+ return this.cmd(["jq", "-r", `
+ ["title", "timestamp"],
+ (
+ .${prop}[]
+ | [.title, (.timestamp | todateiso8601)]
+ )
+ | @csv
+ `])
+}
+function facebook_groups_joined_v1(this: TaskTargetPipelineHelper) {
+ return this.facebook_groups_joined_generic("groups_joined");
+}
+function facebook_groups_joined_v2(this: TaskTargetPipelineHelper) {
+ return this.facebook_groups_joined_generic("groups_joined_v2");
+}
+
+function facebook_group_posts_v1(this: TaskTargetPipelineHelper) {
+ // TODO: Attachments metadata, maybe another timestamp in the data field too (but it looks like the same everywhere)
+ return this.cmd(["jq", "-r", `
+ ["title", "data", "timestamp"],
+ (
+ .group_posts.activity_log_data[]
+ | [.title, "TODO", (.timestamp | todateiso8601)]
+ )
+ | @csv
+ `])
+}
+function facebook_group_posts_v2(this: TaskTargetPipelineHelper) {
+ // TODO: Still a data and attachments to pull out
+ return this.cmd(["jq", "-r", `
+ ["title", "data", "timestamp"],
+ (
+ .group_posts_v2[]
+ | [.title, "TODO", (.timestamp | todateiso8601)]
+ )
+ | @csv
+ `])
+}
+
+function facebook_v2(this: TaskTargetPipelineHelper) {
+ const p = this.setId(t=>`Facebookv2 - ${t.basename}`); // Generic ID for everything in here
+ const col: Set = new Set();
+
+ // No correlary to accounts_and_profiles.json
+ // No correlary for your_off-facebook_activity.json
+ p.collect(col).cd(`apps_and_websites_off_of_facebook/connected_apps_and_websites.json`).read().facebook_installed_apps_v2();
+ p.collect(col).cd(`your_facebook_activity/comments_and_reactions/comments.json`).read().facebook_comments_v2();
+ p.collect(col).glob(`your_facebook_activity/messages/**/*.json`) // Files are message_1.json, etc
+ .setId(t=>`Facebookv2 - Messages ${t.basenameN(2)}`) // 1, 2, etc is not specific enough, include the convo name
+ .read()
+ .facebook_messages_generic()
+
+ p.collect(col).cd(`your_facebook_activity/other_activity/time_spent_on_facebook.json`).read()
+ .cmd(["jq", "-r", `
+ ["start","end"],
+ (
+ .label_values[]
+ | select(.label == "Intervals")
+ | .vec[]
+ | [
+ (.dict[0].timestamp_value | todateiso8601),
+ (.dict[1].timestamp_value | todateiso8601)
+ ]
+ )
+ | @csv
+ `])
+ p.collect(col).cd(`your_facebook_activity/groups/your_group_membership_activity.json`).read().facebook_groups_joined_v2();
+ p.collect(col).cd(`your_facebook_activity/groups/group_posts_and_comments.json`).read().facebook_group_posts_v2();
+ p.collect(col).cd(`your_facebook_activity/pages/pages_and_profiles_you've_unfollowed.json`).read().facebook_pages_unfollowed_v2();
+
+ p.collect(col).cd(`connections/friends/your_friends.json`).read().facebook_friends_generic("friends_v2");
+ p.collect(col).cd(`connections/friends/rejected_friend_requests.json`).read().facebook_friends_generic("rejected_requests_v2");
+ p.collect(col).cd(`connections/friends/received_friend_requests.json`).read().facebook_friends_generic("received_requests_v2");
+
+ p.collect(col).cd(`logged_information/activity_messages/people_and_friends.json`).read().facebook_people_interactions_v2()
+ p.collect(col).cd(`logged_information/search/your_search_history.json`).read().facebook_searches_v2()
+ p.collect(col).cd(`logged_information/notifications/notifications.json`).read().facebook_notifications_v2();
+
+ p.collect(col).cd(`security_and_login_information/account_activity.json`).read().facebook_account_activity_v2()
+ p.collect(col).cd(`security_and_login_information/record_details.json`).read().facebook_admin_records_v2()
+ p.collect(col).cd(`security_and_login_information/where_you're_logged_in.json`).read().facebook_authorized_logins_v2()
+ p.collect(col).cd(`security_and_login_information/email_address_verifications.json`).read().facebook_contact_verification_v2()
+ p.collect(col).cd(`security_and_login_information/logins_and_logouts.json`).read().facebook_account_accesses_v2()
+
+ p.collect(col).cd(`your_facebook_activity/facebook_marketplace/items_sold.json`).read().facebook_marketplace_items_sold_v2()
+
+ return Array.from(col);
+}
+
+function facebook(this: TaskTargetPipelineHelper){
+ const p = this.setId(t=>`Facebook - ${t.basename}`); // Generic ID for everything in here
+ const col: Set = new Set();
+
+ p.collect(col).cd(`about_you/notifications.json`).read().facebook_notifications_v1()
+ //TODO: .fork().skip('face_recognition.json').reason("Not a table, no idea how to use")
+ //TODO: .fork().skip('friend_peer_group.json').reason("Not a table, very small file")
+ //TODO:.fork().skip('messenger.json').reason("Not a table, but might have some juicy stuff for future")
+ //TODO: .fork().todo('preferences.json').reason("Too complex for now")
+ //TODO:.fork().todo('visited.json').reason("Too complex for now")
+ //TODO:.fork().todo('viewed.json').reason("Too complex for now")
+
+ p.collect(col).cd(`accounts_center/accounts_and_profiles.json`).read()
+ .cmd(["jq", "-r", `["service_name","native_app_id","username","email", "phone_number", "name"],
+ (
+ .linked_accounts[]
+ | [.service_name, .native_app_id, .username, .email, .phone_number, .name]
+ )
+ | @csv`])
+ .csvSink()
+
+
+ p.collect(col).cd(`ads_and_businesses/your_off-facebook_activity.json`).read()
+ .cmd(["jq", "-r", `
+ ["name","id","type","timestamp"],
+ (
+ .off_facebook_activity[]
+ | .name as $name
+ | .events[]
+ | [$name, .id, .type, (.timestamp | todateiso8601)]
+ )
+ | @csv
+ `])
+ .csvSink([["timestamp", "numeric"]])
+ //TODO: .fork().todo('advertisers_who_uploaded_a_contact_list_with_your_information.json')
+
+ p.collect(col).cd(`apps_and_websites/apps_and_websites.json`).read().facebook_installed_apps_v1()
+
+ // `${facebookRoot}/archive` - no data in my export
+ // `${facebookRoot}/campus` - no data in my export
+
+ p.collect(col).cd(`comments/comments.json`).read().facebook_comments_v1()
+
+ p.collect(col).glob(`dating/messages/*.json`) // Files are 0.json, 1.json, etc
+ .setId(t=>`Facebook - Dating Messages ${t.basename}`) // Slightly more specific message
+ .read()
+ .cmd(["jq", "-r", `
+ ["from","to","timestamp","body"],
+ .recipient as $to
+ | (
+ .messages[]
+ | ["Me", $to, (.timestamp | todateiso8601), .body]
+ )
+ | @csv
+ `])
+ .csvSink();//[["timestamp", "numeric"]])
+ //todo: your_dating_activity.json, but it only has a few lines and not super useful
+ //todo: the other dating files are also just, small
+
+ // TODO: events
+// rcd(`events`);
+// localCollect('event_invitations.json', json, sspawn('jq', [`
+// .events_invited[] |= (
+// .start_timestamp |= todateiso8601 |
+// .end_timestamp |= todateiso8601
+// )
+// `]));
+// localCollect('your_event_responses.json', json, sspawn('jq', [`
+// .event_responses.events_joined[] |= (
+// .start_timestamp |= todateiso8601 |
+// .end_timestamp |= todateiso8601
+// ) |
+// .event_responses.events_declined[] |= (
+// .start_timestamp |= todateiso8601 |
+// .end_timestamp |= todateiso8601
+// ) |
+// .event_responses.events_interested[] |= (
+// .start_timestamp |= todateiso8601 |
+// .end_timestamp |= todateiso8601
+// )
+// `]));
+
+ p.collect(col).cd(`facebook_gaming/instant_games.json`)
+ .read()
+ .cmd(["jq", "-r", `
+ ["game", "added_timestamp"],
+ (
+ .instant_games_played[]
+ | [.name, (.added_timestamp | todateiso8601)]
+ )
+ | @csv
+ `])
+ .csvSink([["added_timestamp", "numeric"]])
+
+ p.collect(col).cd(`following_and_followers/unfollowed_pages.json`).read().facebook_pages_unfollowed_v1()
+ p.collect(col).cd(`following_and_followers/following.json`)
+ .read()
+ .cmd(["jq", "-r", `
+ ["name", "timestamp"],
+ (
+ .following[]
+ | [.name, (.timestamp | todateiso8601)]
+ )
+ | @csv
+ `])
+ .csvSink([["timestamp", "numeric"]])
+ p.collect(col).cd(`following_and_followers/followers.json`)
+ .read()
+ .cmd(["jq", "-r", `
+ ["name"],
+ (
+ .followers[]
+ | [.name]
+ )
+ | @csv
+ `])
+ .csvSink()
+
+ p.collect(col).cd(`friends/sent_friend_requests.json`).read().facebook_friends_generic("sent_requests")
+ p.collect(col).cd(`friends/removed_friends.json`).read().facebook_friends_generic("deleted_friends")
+ p.collect(col).cd(`friends/rejected_friend_requests.json`).read().facebook_friends_generic("rejected_requests")
+ p.collect(col).cd(`friends/received_friend_requests.json`).read().facebook_friends_generic("received_requests")
+ p.collect(col).cd(`friends/friends.json`).read().facebook_friends_generic("friends")
+
+ p.collect(col).cd(`groups/your_group_membership_activity.json`).read().facebook_groups_joined_v1();
+ p.collect(col).cd(`groups/your_posts_and_comments_in_groups.json`).read().facebook_group_posts_v1();
+
+ // there's also groups.json and events.json but neither has timestamp so they're
+ // not really useful right now
+ p.collect(col).cd(`interactions/people.json`).read().facebook_people_interactions_v1()
+
+ // `${facebookRoot}/journalist_registration` - no data in my export
+
+ p.collect(col).cd(`likes_and_reactions/pages.json`)
+ .read()
+ .cmd(["jq", "-r", `
+ ["name", "timestamp"],
+ (
+ .page_likes[]
+ | [.name, (.timestamp | todateiso8601)]
+ )
+ | @csv
+ `])
+ .csvSink([["timestamp", "numeric"]])
+ p.collect(col).cd(`likes_and_reactions/posts_and_comments.json`)
+ .read()
+ .cmd(["jq", "-r", `
+ ["title", "timestamp", "reaction"],
+ (
+ .reactions[]
+ | [.name, (.timestamp | todateiso8601), .data[0].reaction.reaction]
+ )
+ | @csv
+ `])
+ .csvSink([["timestamp", "numeric"]])
+
+ // TODO:
+ // rcd(`location`);
+ // localCollect('primary_location.json', json);
+ // localCollect('primary_public_location.json', json);
+ // localCollect('timezone.json', json);
+
+ p.collect(col).cd(`marketplace/items_sold.json`).read().facebook_marketplace_items_sold_v1()
+
+
+ p.collect(col).cd(`messages/**/*.json`) // Files are message_1.json, etc
+ .setId(t=>`Facebook - Messages ${t.basenameN(2)}`) // 1, 2, etc is not specific enough, include the convo name
+ .read()
+ .facebook_messages_generic()
+
+
+ // `${facebookRoot}/music_recommendations` - no data
+
+ // rcd(`news`);
+ // localCollect('your_locations.json', json);
+
+ p.collect(col).cd(`other_activity/pokes.json`)
+ .read()
+ .cmd(["jq", "-r", `
+ ["from", "to","rank","timestamp"],
+ (
+ .pokes.data[]
+ | [.poker, .pokee, .rank, (.timestamp | todateiso8601)]
+ )
+ | @csv
+ `])
+ .csvSink([["timestamp", "numeric"]]);
+ p.collect(col).cd(`other_activity/support_correspondences.json`)
+ .read()
+ // TODO: I'm seeing blanks in .from and .to when the replier was Facebook
+ // themselves. Perhaps it's broken?
+ // TODO: Attachments
+ .cmd(["jq", "-r", `
+ ["from", "to", "subject", "message", "timestamp"],
+ (
+ .support_correspondence[].messages[]
+ | [.from, .to, .subject, .message, (.timestamp | todateiso8601)]
+ )
+ | @csv
+ `])
+ .csvSink([["timestamp", "numeric"]])
+
+
+ // `${facebookRoot}/pages` - no data
+
+ p.collect(col).cd(`payment_history/payment_history.json`)
+ .read()
+ .cmd(["jq", "-r", `
+ ["from", "to","amount","currency", "type","status","payment_method", "created_timestamp"],
+ (
+ .payments.payments[]
+ | [.sender, .receiver, .amount, .currency, .type, .status, .payment_method, (.created_timestamp | todateiso8601)]
+ )
+ | @csv
+ `])
+ .csvSink([["created_timestamp", "numeric"]]);
+
+ // TODO: There's also photos_and_videos/your_videos.json
+ // TODO: There's a media_metadata in each of the images too to convert as well as external files
+ p.collect(col).glob(`photos_and_videos/album/*.json`)
+ // Could use a better name, currently 0.json, 1.json, etc...
+ .setId(t=>`Facebook - Album ${t.basename}`) //slightly more speciifc name, it woudl be better if we could use the album name
+ .read()
+ .cmd(["jq", "-r", `
+ ["album","uri","creation_timestamp"],
+ (
+ .photos[]
+ | [.title, .uri, (.creation_timestamp | todateiso8601)]
+ )
+ | @csv
+ `])
+ .csvSink([["creation_timestamp", "numeric"]])
+
+ p.collect(col).cd(`posts/your_pinned_posts.json`)
+ .read()
+ .cmd(["jq", "-r", `
+ ["name","uri","timestamp"],
+ (
+ .pinned_posts[].entries[]
+ | [.data.name, .data.uri, (.timestamp | todateiso8601)]
+ )
+ | @csv
+ `])
+ .csvSink([["timestamp", "numeric"]])
+ // TODO: Glob? I never posted a lot on FB
+ p.collect(col).cd(`posts/your_posts_1.json`)
+ .read()
+ // TODO: Data is an array with objects. .post, .updated_timestamp, separately??
+ // TODO: Also attachments
+ .cmd(["jq", "-r", `
+ ["title","data","timestamp"],
+ (
+ .[]
+ | [.title, "TODO: data", (.timestamp | todateiso8601)]
+ )
+ | @csv
+ `])
+ .csvSink([["timestamp", "numeric"]])
+
+ // `${facebookRoot}/privacy_checkup` - no data
+
+ // TODO: Shape is non-tabular, but maybe we should handle it?
+ // Looks mostly like dupes from other places
+ // './profile_information.json': undefined,
+ // The minimum amount of data is just .title and .timestamp
+ // TODO: HAndle data and attachments
+ p.collect(col).cd(`profile_information/profile_update_history.json`)
+ .read()
+ .cmd(["jq", "-r", `
+ ["title","timestamp"],
+ (
+ .profile_updates[]
+ | [.title, (.timestamp | todateiso8601)]
+ )
+ | @csv
+ `])
+ .csvSink([["timestamp", "numeric"]])
+
+ // `${facebookRoot}/rewards` - no data
+ // `${facebookRoot}/saved_items_and_collections` - no data
+
+ p.collect(col).cd(`search_history/your_search_history.json`).read().facebook_searches_v1()
+
+ p.collect(col).cd(`security_and_login_information/account_status_changes.json`)
+ .read()
+ .cmd(["jq", "-r", `
+ ["status","timestamp"],
+ (
+ .account_status_changes[]
+ | [.status, (.timestamp | todateiso8601)]
+ )
+ | @csv
+ `])
+ .csvSink([["timestamp", "numeric"]])
+ p.collect(col).cd(`security_and_login_information/account_activity.json`).read().facebook_account_activity_v1()
+ p.collect(col).cd(`security_and_login_information/administrative_records.json`).read().facebook_admin_records_v1()
+ p.collect(col).cd(`security_and_login_information/authorized_logins.json`).read().facebook_authorized_logins_v1()
+ p.collect(col).cd(`security_and_login_information/contact_verifications.json`).read().facebook_contact_verification_v1()
+ p.collect(col).cd(`security_and_login_information/logins_and_logouts.json`).read().facebook_account_accesses_v1()
+ // TODO: datr_cookie_info, looks like a bunch of timestamps
+ // a.fork().cd(`login_protection_data.json`)
+ // .read()
+ // // TODO: updated_timestamp doesn't always exist
+ // .cmd(["jq", "-r", `
+ // ["name", "created_timestamp", "updated_timestamp", "ip_address"],
+ // (
+ // .login_protection_data[]
+ // | [.name, (.created_timestamp | todateiso8601), (.updated_timestamp | todateiso8601), .ip_address]
+ // )
+ // | @csv
+ // `])
+ // TODO: mobile_devices, only a couple entries
+ // TODO: used_ip_addresses
+ // TODO: where_you've logged in
+ // TODO: your_facebook_activity, useless and small
+
+
+ // `${facebookRoot}/short_videos` - no data in my export
+ // `${facebookRoot}/saved_items_and_collections` - no data in my export
+
+ p.collect(col).cd(`stories/story_reactions.json`)
+ .read()
+ .cmd(["jq", "-r", `
+ ["title", "timestamp"],
+ (
+ .stories_feedback[]
+ | [.title, (.timestamp | todateiso8601)]
+ )
+ | @csv
+ `])
+ .csvSink([["timestamp", "numeric"]])
+
+ // `${facebookRoot}/trash` - no data in my export
+ // `${facebookRoot}/voice_recording_and_transcription` - no data in my export
+ // `${facebookRoot}/volunteering` - no data in my export
+ // `${facebookRoot}/voting_location_and_reminders` - only small 1-property things
+ // `${facebookRoot}/your_places` - no data in my export
+ // `${facebookRoot}/your_topics` - no data in my export
+
+ return Array.from(col);
+};
+
diff --git a/data-export/facebook2.ts b/data-export/facebook2.ts
deleted file mode 100644
index d65c785..0000000
--- a/data-export/facebook2.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-import { Task } from "./task.ts";
-
-export function facebook(this: Task, path: string){
- const t = this.fork();
- t.cd(path);
-
- t.fork().cd(`about_you`)
- .fork().skip('face_recognition.json').reason("Not a table, no idea how to use")
- .fork().skip('friend_peer_group.json').reason("Not a table, very small file")
-
- .fork().skip('messenger.json').reason("Not a table, but might have some juicy stuff for future")
- .fork().read('notifications.json')
- .jq(["-r", `["timestamp","unread","href","text"],
- (
- .notifications[]
- | [(.timestamp | todateiso8601), .unread, .href, .text]
- )
- | @csv`])
- // .write(`${process.cwd()}/probe.csv`)
- .sqlite_utils(["insert", "your.db", "notifications.json", "-", "--csv", "--detect-types"])
- .sink()
-
- .fork().todo('preferences.json').reason("Too complex for now")
-
- .fork().todo('visited.json').reason("Too complex for now")
-
- .fork().todo('viewed.json').reason("Too complex for now")
-
- t.fork().cd(`accounts_center`)
- .fork().read('accounts_and_profiles.json')
- .jq(["-r", `["service_name","native_app_id","username","email", "phone_number", "name"],
- (
- .linked_accounts[]
- | [.service_name, .native_app_id, .username, .email, .phone_number, .name]
- )
- | @csv`])
- .sqlite_utils(["insert", "your.db", "accounts_and_profiles.json", "-", "--csv", "--no-headers", "--detect-types"])
- .sink()
-
-
- t.fork().cd(`ads_and_businesses`)
- .fork().todo('advertisers_who_uploaded_a_contact_list_with_your_information.json')
- .fork().read('your_off-facebook_activity.json')
- .jq(["-r", `
- ["name","id","type","timestamp"],
- (
- .off_facebook_activity[]
- | .name as $name
- | .events[]
- | [$name, .id, .type, (.timestamp | todateiso8601)]
- )
- | @csv
- `])
- .sqlite_utils(["insert", "your.db", "your_off-facebook_activity.json", "-", "--csv", "--no-headers", "--detect-types"])
- .sink()
-
- return t;
-};
-
diff --git a/data-export/google.ts b/data-export/google.ts
new file mode 100644
index 0000000..bffe82b
--- /dev/null
+++ b/data-export/google.ts
@@ -0,0 +1,107 @@
+import { TaskTargetPipelineHelper } from "./task.ts";
+import { htmlSelectorChunkedDuplex } from "./html.ts";
+
+export function google(this: TaskTargetPipelineHelper){
+ const p = this.setId(t=>`Google - ${t.basename}`); // Generic ID for everything in here
+ const col: Set = new Set();
+
+ // TODO: There is a root takeout folder
+
+
+ p.collect(col).cd('Access Log Activity/Activities - A list of Google services accessed by.csv').read()
+ p.collect(col).cd('Devices - A list of devices (i.e. Nest, Pixel, iPh.csv').read()
+
+ // Assignments - data was empty
+ // Business messages - GMB messages, there's some but so far outside of what I want
+ // TODO: Calendar, exports an .ics
+
+ // a = t.fork().cd(`Chrome`)
+ // TODO: Assersses and mode.json
+ // TODO: Bookmarks.csv
+ // TODO: Device Information.json
+ // TODO: Dictionary.csv
+ // TODO: ...
+ p.collect(col).cd('Chrome/History.json')
+ .read()
+ // TODO: Typed Url", no data
+ // TODO: "session", complex data
+ // Omitted .ptoken and .client_id for now. I think ptoken is maybe for the history API? client_id is base64 something...
+ // TODO: time_usec IS WRONG!! Needs to be ms
+ .cmd(["jq", "-r", `["favicon_url","page_transition","title","url","time_usec"],
+ (
+ ."Browser History"[]
+ | [.favicon_url, .page_transition, .title, .url, (.time_usec | todateiso8601)]
+ )
+ | @csv`])
+
+ // TODO: Contactss, exports an .vcf
+ // TODO: ...
+
+ // a = t.fork().cd(`Google Pay`)
+ p.collect(col).cd(`Google Pay/Google transactions`).glob(`transactions_*.csv`)
+ .read()
+ .csvSink()
+ // .fork("a").cd(`Money sends and requests`)
+ // .fork().cd(`Money sends and requests.csv`)
+ // .read()
+ // .cmd(t=>["sqlite-utils", "insert", "your.db", t.basename, "-", "--csv", "--detect-types"])
+ // TODO: One more folder, and it only has a pdf
+
+ // TODO: Google Play Movies _ TV - no data
+ // TODO: ...
+
+ p.collect(col).cd("Location History/Location History.json")
+ .read()
+ // TODO: This is missing
+ // "altitude" : 158,
+ // "verticalAccuracy" : 68
+ // and the activity models. I had no idea google tries to determine if I'm "tilting"
+ .cmd(["jq", "-r", `["timestamp","latitudeE7","longitudeE7","accuracy"],
+ (
+ .locations[]
+ | [.timestampMs | todateiso8601, .latitudeE7, .longitudeE7, .accuracy]
+ )
+ | @csv`])
+ .csvSink()
+ // There's also the semantic history but that's an entire nother can of worms
+ // it seems like
+
+ // TODO: Needs no-headers!
+ // a = t.fork().cd(`My Activity`)
+ // a.fork().glob(`**/MyActivity.html`)
+ // .setId(t=>`Google - ${t.basenameN(2)}`)
+ // .read()
+ // .pipe(()=>{
+ // // Parses the MyActivity format, chunking it into pieces of HTML text
+ // // and then parsing out the text
+ // const dup = htmlSelectorChunkedDuplex(
+ // (tag, attrs)=>{
+ // // TODO: We also probably want to get and parse each
+ // // ".content-cell.mdl-typography--caption" as well (it
+ // // has location for websearches and sometimes a details field)
+ // // but then we have to get ".mdl-grid" and parse it
+ // return attrs.class?.includes("content-cell")
+ // && attrs.class?.includes("mdl-typography--body-1")
+ // && !attrs.class?.includes("mdl-typography--text-right")
+ // },
+ // (chunk)=>{
+ // const text = chunk.innerText;
+ // const split = text.split("\n");
+ // const timestamp = split.pop(); // TODO: need to parse this
+ // const rest = split.join("\n");
+ // // TODO: Escape instead of replace
+ // const restSafe = rest.replace(/"/g, "'").replace(/\n/g,"\\n"); // escape newlines and quotes
+ // // Return a CSV
+ // return `"${restSafe}","${timestamp}"\n`;
+ // }
+ // );
+ // return dup;
+ // })
+
+ // TODO: News
+ // TODO: Profile
+ // TODO: Tasks - No data
+
+ return Array.from(col);
+};
+
diff --git a/data-export/html.ts b/data-export/html.ts
new file mode 100644
index 0000000..afe46f9
--- /dev/null
+++ b/data-export/html.ts
@@ -0,0 +1,152 @@
+import { strict as assert } from "node:assert";
+import { Parser as HTMLParser2 } from "htmlparser2";
+import { WritableStream } from "htmlparser2/WritableStream";
+import { Duplex, Readable, Writable } from 'node:stream';
+import duplexify from "duplexify";
+type HTMLParser2CBs = ConstructorParameters[0];
+type HTMLParser2Opts = ConstructorParameters[1];
+type HTMLParser2Args = ConstructorParameters;
+
+
+const htmlVoidElements = [
+ 'area',
+ 'base',
+ 'basefont',
+ 'bgsound',
+ 'br',
+ 'col',
+ 'command',
+ 'embed',
+ 'frame',
+ 'hr',
+ 'image',
+ 'img',
+ 'input',
+ 'isindex',
+ 'keygen',
+ 'link',
+ 'menuitem',
+ 'meta',
+ 'nextid',
+ 'param',
+ 'source',
+ 'track',
+ 'wbr',
+];
+
+export function openTag(tagName: string, attributes: {[k: string]: string}) {
+ // Transform attributes into string
+ let attrs = Object.entries(attributes)
+ .map(([k,v])=>{
+ // If the HTML coming through uses single quotes for the attribute, it
+ // can contain a double quote, so just escape those. Markdown-it generates
+ // this if you use a " in an alt tag, but I also do this sometimes too
+ v = v
+ .replace(/"/g, '"');
+ return `${k}="${v}"`;
+ })
+ .join(' ');
+ attrs = attrs ? ' ' + attrs : '';
+ // self close certain things, because JSX requires all tags to be closed,
+ // no html
+ const selfClosing = htmlVoidElements.includes(tagName) ? ' /' : '';
+ return `<${tagName}${attrs}${selfClosing}>`;
+}
+
+export function closeTag(tagName: string) {
+ if (htmlVoidElements.includes(tagName)) {
+ // No closing tag
+ return '';
+ }
+
+ return `${tagName}>`;
+}
+
+interface HTMLChunk {
+ innerText: string;
+ innerHTML: string;
+}
+
+function htmlSelectorChunker(matcher: (tag: string, attrs:{ [s: string]: string })=>boolean, cb: (chunk: HTMLChunk)=>void): HTMLParser2CBs {
+ let tagStateStack: {
+ tag: string,
+ attrs: { [s: string]: string },
+ marked?: boolean,
+ innerText?: string,
+ innerHTML?: string
+ }[] = [];
+ const htmlParser2CBs: HTMLParser2CBs = {
+ onopentag(tag, attrs) {
+ const marked = tagStateStack.find(t => t.marked);
+
+ const tagStackItem = {
+ tag, attrs
+ };
+ if (matcher(tag, attrs)) {
+ assert(!marked, "Nested tag marking encountered, not implemented/no sane implementation");
+ (tagStackItem as any).marked = true;
+ (tagStackItem as any).innerText = "";
+ (tagStackItem as any).innerHTML = "";
+ }
+ tagStateStack.push(tagStackItem);
+
+ if (marked) {
+ marked.innerHTML += openTag(tag, attrs);
+ const str = tag === "br" ? "\n" : "";
+ marked.innerText += str;
+ }
+
+ },
+ ontext(text) {
+ const marked = tagStateStack.find(t => t.marked);
+ if (!marked) {
+ return; // nothing to do
+ }
+
+ marked.innerText += text;
+ },
+ onclosetag(tag) {
+ const marked = tagStateStack.find(t => t.marked);
+ if (!marked) {
+ return;
+ }
+ marked.innerHTML += closeTag(tag);
+ const popped = tagStateStack.pop();
+ if (marked === popped) {
+ cb(popped as HTMLChunk);
+ }
+ },
+ };
+ return htmlParser2CBs;
+}
+
+class ExternalReadable extends Readable {
+ _read() {
+
+ }
+}
+
+export function htmlSelectorChunkedDuplex(
+ matcher: (tag: string, attrs:{ [s: string]: string })=>boolean,
+ postProcess: (chunk: HTMLChunk)=>string
+): Duplex {
+ const readable = new ExternalReadable();
+ const cbs = htmlSelectorChunker(
+ matcher,
+ (chunk)=>{
+ const out = postProcess(chunk);
+ readable.push(out);
+ }
+ );
+ const writable = new WritableStream({
+ ...cbs,
+ onerror(error){
+ readable.emit("error", error);
+ },
+ onend() {
+ readable.push(null);
+ readable.emit("close");
+ }
+ });
+ return duplexify(writable, readable);
+}
diff --git a/data-export/task-before-functional.ts.old b/data-export/task-before-functional.ts.old
new file mode 100644
index 0000000..225cac9
--- /dev/null
+++ b/data-export/task-before-functional.ts.old
@@ -0,0 +1,352 @@
+import nodePath from 'node:path';
+import fs from 'node:fs';
+import { strict as assert } from "node:assert";
+import { execFile as _execFile } from "node:child_process";
+import { promisify } from "node:util";
+import { ZipFS } from "./zipFs.ts";
+import { globSync } from "glob";
+
+const execFile = promisify(_execFile);
+
+type FSImpl = {
+ isZip?: boolean;
+ zipPath?: string;
+ init?(): Promise;
+ ready?: boolean;
+
+ statSync: typeof fs["statSync"];
+ existsSync: typeof fs["existsSync"];
+
+ // Required by glob
+ lstatSync: typeof fs["lstatSync"];
+ // Needs to include withFileTypes DirEnt variant
+ readdir: typeof fs["readdir"];
+ readdirSync: typeof fs["readdirSync"];
+ readlinkSync: typeof fs["readlinkSync"];
+ realpathSync: typeof fs["realpathSync"];
+ promises: {
+ lstat: typeof fs.promises["lstat"];
+ // Needs to include withFileTypes DirEnt
+ readdir: typeof fs.promises["readdir"];
+ readlink: typeof fs.promises["readlink"];
+ realpath: typeof fs.promises["realpath"];
+ }
+};
+const defaultFSImpl = fs;
+
+function safe(s: string) {
+ return s.replace(/[^a-zA-Z0-9_]/g, '_');
+}
+
+
+//TODO: DANGER: I doubt this is safe...
+function shEscape(s: string) {
+ assert(!s.includes("\n"), "shEscape given new line, caller needs to handle these");
+ if (!s.match(/[ \$\"\'\!]/)) {
+ return s;
+ }
+ // We need to quote this string
+ // Single quoted strings require you to close the single quoted string, then
+ // use the escaped single quote, and then reopen the string... obscene
+ s = s.replace(/'/g, "'\\''");
+ s = `'${s}'`;
+ return s;
+}
+
+abstract class TaskTargetBase {
+ target: TaskTarget;
+ constructor(target: TaskTarget) {
+ this.target = target;
+ }
+ abstract get type(): "read" | "mid";
+ abstract toShell(): string;
+}
+class TaskTargetRead extends TaskTargetBase {
+ get type(){ return "read" as const; }
+ toShell() {
+ if (this.target.fsImpl.isZip) {
+ assert(this.target.fsImpl.zipPath, "Should have a zipPath");
+ // We need to be able to do this
+ return `7z x ${shEscape(this.target.fsImpl.zipPath)} -so ${shEscape(this.target.path)}`;
+ }
+
+ // TODO : Implement when reading from a zip file
+ return `cat ${shEscape(this.target.path)}`;
+ }
+}
+class TaskTargetCmd extends TaskTargetBase {
+ get type(){ return "mid" as const; }
+ /**What nodejs spawn() and execFile() take
+ * [cmd, ...args]: string[]
+ */
+ cmd: string[];
+ static parse(target: TaskTarget, v: string | string[] | ((t: TaskTarget)=>string) | ((t: TaskTarget)=>string[])): string[] {
+ if (typeof v === "function") {
+ v = v(target);
+ }
+ if (typeof v === "string") {
+ v = v.split(/\s+/);
+ }
+ return v;
+ }
+ constructor(target: TaskTarget, cmd: string | string[] | ((t: TaskTarget)=>string) | ((t: TaskTarget)=>string[])) {
+ super(target);
+ this.cmd = TaskTargetCmd.parse(target, cmd);
+ }
+ toShell() {
+ const out = this.cmd
+ .map(c => {
+ let sh = c.replace(/\n/g, "")
+ return shEscape(sh);
+ });
+
+ return out.join(" ");
+ }
+}
+
+
+
+class TaskTarget {
+ path: string;
+ fsImpl: FSImpl = defaultFSImpl;
+ pipeline: TaskTargetBase[];
+ idValue: string | ((t: TaskTarget)=>string) | undefined;
+ postFns: ((t: TaskTarget)=>Promise)[];
+
+ constructor(path: string){
+ this.path = path;
+ this.pipeline = [];
+ this.postFns = [];
+ }
+
+ exists() {
+ return this.fsImpl.existsSync(this.path);
+ }
+
+ _joinPath(path: string) {
+ let finalPath = path;
+ if (!path.startsWith('/')) {
+ finalPath = nodePath.join(this.path, path)
+ }
+ return finalPath;
+ }
+
+ get basename() {
+ return safe(nodePath.basename(this.path));
+ }
+ basenameN(n: number) {
+ return this.path
+ .split("/")
+ .map(s => safe(s))
+ .slice(-n)
+ .join("___");
+ }
+
+ get id() {
+ assert(this.idValue, `TaskTarget for path "${this.path}" must have an id`);
+ if (typeof this.idValue === "function") {
+ return safe(this.idValue(this));
+ }
+ return safe(this.idValue);
+ }
+
+ /**Changes the current directory of the target*/
+ cd(path: string) {
+ this.path = this._joinPath(path);
+ }
+
+ /**Get a glob off of the target*/
+ glob(globPath: string) {
+ globPath = this._joinPath(globPath);
+ return globSync(globPath, {
+ cwd: '/DUMMYCWD',
+ fs: this.fsImpl
+ });
+ }
+
+ clone() {
+ const t = new TaskTarget(this.path);
+ t.fsImpl = this.fsImpl;
+ t.idValue = typeof this.idValue === "function" ? this.idValue : undefined;
+ t.postFns = t.postFns.slice();
+ //TODO: clone pipeline
+ return t;
+ }
+
+ pushToPipeline(v: TaskTargetBase) {
+ if (v.type === "read") {
+ assert(this.pipeline.length === 0, "A read can only be the first item in a pipeline");
+ }
+
+ this.pipeline.push(v);
+ }
+
+ pushPostFn(fn: ((t: TaskTarget)=>Promise)) {
+ this.postFns.push(fn);
+ }
+}
+
+/**A very composable object*/
+export class Task {
+ /**A serial pipeline of Streams*/
+ targets: TaskTarget[];
+
+ /**SHARED list of all tasks for this given tree*/
+ tasks: Task[];
+
+ constructor() {
+ this.tasks = [];
+ this.targets = [new TaskTarget(process.cwd())];
+ }
+
+ cd(path: string) {
+ for (const t of this.targets) {
+ // TODO: opts
+ t.cd(path);
+ }
+ return this;
+ }
+
+ /**Globs for all the paths that match under all targets*/
+ glob(globPath: string) {
+ // For every target, concat glob onto it, glob, and then
+ // replace the original set of targets with all the new ones
+ const newTargets: TaskTarget[] = [];
+ for (const t of this.targets) {
+ const matches = t.glob(globPath);
+ for (const m of matches) {
+ const newT = t.clone();
+ newT.path = m;
+ newTargets.push(newT);
+ }
+ }
+ this.targets = newTargets;
+ return this;
+ }
+
+ /**Opens all targets as zip archives*/
+ async zip() {
+ for (const t of this.targets) {
+ const zfs = new ZipFS(t.path);
+ await zfs.init();
+ t.path = ""; // Each target is now rooted at the base of its respective zip
+ t.fsImpl = zfs.getImpl() as any;
+ }
+ return this;
+ }
+
+ /**Returns a copy of ourself*/
+ clone() {
+ const t = new Task();
+ t.targets = this.targets.map(t => t.clone());
+ t.tasks = this.tasks; //SHARED object reference
+ return t;
+ }
+
+ /**Returns a copy of ourself, but adds us to this tree's shared
+ * task list as well*/
+ fork() {
+ const c = this.clone();
+ this.tasks.push(c);
+ return c;
+ }
+
+ cmd(cmd: string | string[] | ((target: TaskTarget)=>string) | ((target: TaskTarget)=>string[])) {
+ for (const t of this.targets) {
+ t.pushToPipeline(new TaskTargetCmd(t, cmd));
+ }
+ return this;
+ }
+ read() {
+ for (const t of this.targets) {
+ t.pushToPipeline(new TaskTargetRead(t));
+ }
+ return this;
+ }
+ setId(idValue: string | ((t: TaskTarget)=>string)) {
+ for (const t of this.targets) {
+ t.idValue = idValue;
+ }
+ return this;
+ }
+ post(fn: any) {
+ for (const t of this.targets) {
+ t.pushPostFn(fn);
+ }
+ }
+ types(
+ types: string[]
+ ) {
+ // TODO:
+ return this;
+ }
+ csvSink(
+ summarization?: [string, string][]
+ ) {
+ // Ingest this csv into the database at the given id
+ // this.cmd(t=>["sqlite-utils", "insert", "your.db", t.id, "-", "--csv", "--detect-types"]);
+ // Add a post processing function for these targets that prints out the summarization
+ // stats
+ this.post(async (t: TaskTarget)=>{
+ // We only do the first one so far for the summarization
+ let queryLine: string;
+ let formatFn: (r: any)=>string;
+ const [columnName, type] = summarization?.[0] ?? [undefined, undefined];
+ if (type === "numeric") {
+ queryLine = `min(${columnName}) as lo, max(${columnName}) as hi, count(*) as n`;
+ formatFn = (r: any)=>`${r.n} rows from ${r.lo} to ${r.hi} for ${t.id}`;
+ }
+ else {
+ queryLine = `count(*) as n`;
+ formatFn = (r: any)=>`${r.n} rows for ${t.id}`;
+ }
+
+ const cmd = "sqlite-utils";
+ const args = ["query", "your.db", `select ${queryLine} from ${t.id}`]
+ const { stdout, stderr } = await execFile(cmd, args);
+ const results = JSON.parse(stdout);
+ const result = results[0]; // should only be one result in the array for this type of query
+ const logLine = formatFn(result);
+ (t as any).log = logLine;
+ });
+
+ return this;
+ }
+
+ /**Collect all the TaskTargets, make sure everything is init'd and exists
+ * and output the targets for processing*/
+ async getFinalTargets() {
+ const targets: TaskTarget[] = [];
+ for (const task of this.tasks) {
+ for (const t of task.targets) {
+ // Make sure fsImpl is ready
+ if ("ready" in t.fsImpl && !t.fsImpl.ready && t.fsImpl.init) {
+ await t.fsImpl.init();
+ }
+ if (t.pipeline.length <= 0) {
+ continue; // Tasks with empty pipelines are no-ops, remove
+ }
+ if (!t.exists()) {
+ console.warn(`Missing target ${t.path}`);
+ continue;
+ }
+
+ targets.push(t);
+ }
+ }
+ return targets;
+ }
+
+ async getTaskTSVShell() {
+ const targets = await this.getFinalTargets();
+ let out: string[] = [];
+ for (const t of targets) {
+ const shell = t.pipeline
+ .map(p => p.toShell())
+ .join(" | ")
+ out.push(`${t.id}\t${shell}`);
+ }
+
+ return out.join("\n");
+ }
+}
diff --git a/data-export/task.ts b/data-export/task.ts
index 3e432cb..3e9983e 100644
--- a/data-export/task.ts
+++ b/data-export/task.ts
@@ -1,195 +1,432 @@
import nodePath from 'node:path';
import fs from 'node:fs';
import { strict as assert } from "node:assert";
-import { type SpawnOptions } from "node:child_process";
-import { type Stream } from 'node:stream';
-import { ChildProcessDuplex } from "./util.ts";
-import { finished } from "node:stream/promises";
+import { ZipFS } from "./zipFs.ts";
+import { globSync } from "glob";
+import { $ } from "zx";
-export function pipeCollection(...args: Stream[]) {
- if (args.length <= 1) {
- return args[0];
- }
+type FSImpl = {
+ isZip?: boolean;
+ zipPath?: string;
+ init?(): Promise;
+ ready?: boolean;
- let out = args[0];
- for (const s of args.slice(1)) {
- out = out.pipe(s as any);
+ statSync: typeof fs["statSync"];
+ existsSync: typeof fs["existsSync"];
+
+ // Required by glob
+ lstatSync: typeof fs["lstatSync"];
+ // Needs to include withFileTypes DirEnt variant
+ readdir: typeof fs["readdir"];
+ readdirSync: typeof fs["readdirSync"];
+ readlinkSync: typeof fs["readlinkSync"];
+ realpathSync: typeof fs["realpathSync"];
+ promises: {
+ lstat: typeof fs.promises["lstat"];
+ // Needs to include withFileTypes DirEnt
+ readdir: typeof fs.promises["readdir"];
+ readlink: typeof fs.promises["readlink"];
+ realpath: typeof fs.promises["realpath"];
}
- return out;
+};
+const defaultFSImpl = fs;
+
+function safe(s: string) {
+ return s.replace(/[^a-zA-Z0-9_]/g, '_');
}
-/**A very composable object*/
-export class Task {
- cwd: string;
- /**A serial pipeline of tasks*/
- pipeline: any[];
-
- /**A SHARED object reference between all Task objects of a given tree*/
- tasks: any[];
-
- constructor() {
- this.pipeline = [];
- this.tasks = [];
- this.cwd = process.cwd();
+//TODO: DANGER: I doubt this is safe...
+function shEscape(s: string) {
+ assert(!s.includes("\n"), "shEscape given new line, caller needs to handle these");
+ if (!s.match(/[ \$\"\'\!]/)) {
+ return s;
}
+ // We need to quote this string
+ // Single quoted strings require you to close the single quoted string, then
+ // use the escaped single quote, and then reopen the string... obscene
+ s = s.replace(/'/g, "'\\''");
+ s = `'${s}'`;
+ return s;
+}
- /**Changes the current directory*/
- cd(path: string, opts?: { canFail?: boolean }) {
- if (path.startsWith('/')) {
- this.cwd = path;
- return this;
+interface TaskTargetOp {
+ type: "read" | "mid";
+ toShell(target: TaskTarget): string;
+ clone(): TaskTargetOp;
+}
+class TaskTargetRead implements TaskTargetOp {
+ get type(){ return "read" as const; }
+ toShell(target: TaskTarget) {
+ if (target.fsImpl.isZip) {
+ assert(target.fsImpl.zipPath, "Should have a zipPath");
+ // We need to be able to do this
+ return `7z x ${shEscape(target.fsImpl.zipPath)} -so ${shEscape(target.path)}`;
}
- this.cwd = nodePath.join(this.cwd, path);
+ // TODO : Implement when reading from a zip file
+ return `cat ${shEscape(target.path)}`;
+ }
+ clone() {
+ return new TaskTargetRead();
+ }
+}
+
+type ValidCmd = string | string[] | ((t: TaskTarget)=>string) | ((t: TaskTarget)=>string[]);
+class TaskTargetCmd implements TaskTargetOp {
+ get type(){ return "mid" as const; }
+ /**What nodejs spawn() and execFile() take
+ * [cmd, ...args]: string[]
+ */
+ cmd: ValidCmd;
+ static parse(target: TaskTarget, v: string | string[] | ((t: TaskTarget)=>string) | ((t: TaskTarget)=>string[])): string[] {
+ if (typeof v === "function") {
+ v = v(target);
+ }
+ if (typeof v === "string") {
+ v = v.split(/\s+/);
+ }
+ return v;
+ }
+ constructor(cmd: ValidCmd) {
+ this.cmd = cmd;
+ }
+ toShell(target: TaskTarget) {
+ const parsedCmd = TaskTargetCmd.parse(target, this.cmd);
+ const out = parsedCmd
+ .map(c => {
+ let sh = c.replace(/\n/g, "")
+ return shEscape(sh);
+ });
+
+ return out.join(" ");
+ }
+ clone() {
+ return new TaskTargetCmd(this.cmd);
+ }
+}
+
+type ValidId = string | ((t: TaskTarget)=>string);
+export class TaskTarget {
+ path: string;
+ fsImpl: FSImpl = defaultFSImpl;
+ pipeline: TaskTargetOp[];
+ idValue: ValidId | undefined;
+ postFns: ((t: TaskTarget)=>Promise)[];
+
+ constructor(path: string){
+ this.path = path;
+ this.pipeline = [];
+ this.postFns = [];
+ }
+
+ exists() {
+ return this.fsImpl.existsSync(this.path);
+ }
+
+ _joinPath(path: string) {
+ let finalPath = path;
+ if (!path.startsWith('/')) {
+ finalPath = nodePath.join(this.path, path)
+ }
+ return finalPath;
+ }
+
+ get basename() {
+ return safe(nodePath.basename(this.path));
+ }
+ basenameN(n: number) {
+ return this.path
+ .split("/")
+ .map(s => safe(s))
+ .slice(-n)
+ .join("___");
+ }
+
+ get id() {
+ assert(this.idValue, `TaskTarget for path "${this.path}" must have an id`);
+ if (typeof this.idValue === "function") {
+ return safe(this.idValue(this));
+ }
+ return safe(this.idValue);
+ }
+
+ /**Changes the current directory of the target*/
+ cd(path: string): TaskTarget {
+ this.path = this._joinPath(path);
+ return this;
+ }
+ /**Unzips the file pointed to by the current TaskTarget*/
+ async unzip(): Promise {
+ const zfs = new ZipFS(this.path);
+ await zfs.init();
+ this.path = ""; // target is now rooted at the base of its respective zipfs
+ this.fsImpl = zfs.getImpl() as any;
return this;
}
- clone() {
- const t = new Task();
- t.cwd = this.cwd;
+ /**Get a glob off of the target*/
+ glob(globPath: string): TaskTarget[] {
+ globPath = this._joinPath(globPath);
+ const items = globSync(globPath, {
+ cwd: '/DUMMYCWD',
+ fs: this.fsImpl
+ });
+ const ret = items.map(i => new TaskTarget(i));
+ // TODO: This should probably clone()
+ ret.forEach(t => t.fsImpl = this.fsImpl); // Should all use the same fsImpl
+ return ret;
+ }
+
+ /**Clones the TaskTarget*/
+ clone(): TaskTarget {
+ const t = new TaskTarget(this.path);
+ t.fsImpl = this.fsImpl; // holds no state, just needs same impl
+ t.idValue = this.idValue;
+ t.postFns = t.postFns.slice();
+ t.pipeline = t.pipeline.slice()
+ .map(p => p.clone());
return t;
}
- /**Returns a copy of ourself*/
- fork() {
- const c = this.clone();
- this.tasks.push(c);
- c.tasks = this.tasks; // Share object reference
- return c;
- }
-
- reason(msg: string) {
- // TODO: Add .reason
- //this.reason = msg;
- return this;
- }
- skip(msg: string) {
- // TODO :Print out reaosn too
- this.pipeline.push({
- type: 'fn',
- value: ()=>console.log(`SKIPPED: ${msg}`)
- });
- return this;
- }
- todo(msg: string) {
- // TODO :Print out reaosn too
- this.pipeline.push({
- type: 'fn',
- value: ()=>console.log(`TODO: ${msg}`)
- });
- return this;
- }
-
- jq(cmd: string | string[]) {
- this.pipeline.push({
- type: 'cmd',
- value: Array.isArray(cmd) ? cmd : [cmd],
- cmd: `jq`
- });
- return this;
- }
- sqlite_utils(cmd: string | string[]) {
- this.pipeline.push({
- type: 'cmd',
- value: Array.isArray(cmd) ? cmd : [cmd],
- cmd: `sqlite-utils`
- });
- return this;
- }
- read(path: string) {
- let finalPath = path;
- if (!path.startsWith('/')) {
- finalPath = nodePath.join(this.cwd, path)
+ pushToPipeline(v: TaskTargetOp) {
+ if (v.type === "read") {
+ assert(this.pipeline.length === 0, "A read can only be the first item in a pipeline");
}
- this.pipeline.push({
- type: 'read',
- value: finalPath
- });
+ this.pipeline.push(v);
+ }
+ toShell() {
+ const shell = this.pipeline
+ .map(p => p.toShell(this))
+ .join(" | ")
+ return shell;
+ }
+
+ pushPostFn(fn: ((t: TaskTarget)=>Promise)) {
+ this.postFns.push(fn);
+ }
+
+ cmd(cmd: ValidCmd) {
+ this.pushToPipeline(new TaskTargetCmd(cmd));
return this;
}
- write(path: string) {
- let finalPath = path;
- if (!path.startsWith('/')) {
- finalPath = nodePath.join(this.cwd, path)
- }
-
- this.pipeline.push({
- type: 'write',
- value: finalPath
- });
-
+ read() {
+ this.pushToPipeline(new TaskTargetRead());
return this;
}
- sink() {
+ setId(idValue: ValidId) {
+ this.idValue = idValue;
return this;
}
-
- doPipeline() {
- // Do what's described in .pipeline
- let streams: Stream[] = [];
- let streamDescription = [];
- for (const c of this.pipeline) {
- switch(c.type) {
- case 'fn':
- c.value();
- break;
- case 'cmd':
- streams.push(
- new ChildProcessDuplex(c.cmd, c.value)
- );
- streamDescription.push(`Shell ${c.cmd} ${c.value.join(" ")}`);
- break;
- case 'read':
- streams.push(
- fs.createReadStream(c.value, 'utf8')
- );
- streamDescription.push(`Read ${c.value}`);
- break;
- case 'write':
- streams.push(
- fs.createWriteStream(c.value, 'utf8')
- );
- streamDescription.push(`Write ${c.value}`);
- break;
- default:
- throw new Error(`Unexpected ${c.type}`);
- }
- }
-
- if (streams.length === 0) {
- return undefined;
- }
-
- console.log("About to run:\n" + streamDescription.join("\n"));
-
- const finalStream = pipeCollection(...streams);
- finalStream.on('error', (err: any)=>{
- console.log("IT ERRORED", err)
- });
- return finalStream;
+ post(fn: any) {
+ this.pushPostFn(fn);
}
+ types(
+ types: string[]
+ ) {
+ // TODO:
+ return this;
+ }
+ csvSink(
+ summarization?: [string, string][]
+ ) {
+ // TODO:
+ return this;
- async doTasks() {
- let out = [];
- console.log(`Working on ${this.tasks.length} tasks`);
- for (const t of this.tasks) {
- const s = t.doPipeline();
- if (!s) {
- continue;
- }
- const p = finished(s);
- // await p;
- out.push(p);
- }
- await Promise.all(out);
+ // Ingest this csv into the database at the given id
+ // this.cmd(t=>["sqlite-utils", "insert", "your.db", t.id, "-", "--csv", "--detect-types"]);
+ // Add a post processing function for these targets that prints out the summarization
+ // stats
+ // this.post(async (t: TaskTarget)=>{
+ // // We only do the first one so far for the summarization
+ // let queryLine: string;
+ // let formatFn: (r: any)=>string;
+ // const [columnName, type] = summarization?.[0] ?? [undefined, undefined];
+ // if (type === "numeric") {
+ // queryLine = `min(${columnName}) as lo, max(${columnName}) as hi, count(*) as n`;
+ // formatFn = (r: any)=>`${r.n} rows from ${r.lo} to ${r.hi} for ${t.id}`;
+ // }
+ // else {
+ // queryLine = `count(*) as n`;
+ // formatFn = (r: any)=>`${r.n} rows for ${t.id}`;
+ // }
- // const startTime = performance.now();
- // const stats = final.getStats();
- // const duration = performance.now() - startTime;
- // const thisNodesPerSecond = stats.nodes.size / (duration / 1000);
- // console2g.log(`Loaded ${solved.cacheKey} nodes=${stats.nodes.size} (${thisNodesPerSecond.toFixed(0)}/s) connections=${stats.connections.size} duration=${duration.toFixed(1)}`);
+ // const cmd = "sqlite-utils";
+ // const args = ["query", "your.db", `select ${queryLine} from ${t.id}`]
+ // const { stdout, stderr } = await execFile(cmd, args);
+ // const results = JSON.parse(stdout);
+ // const result = results[0]; // should only be one result in the array for this type of query
+ // const logLine = formatFn(result);
+ // (t as any).log = logLine;
+ // });
+
+ // return this;
}
}
+
+export function each(targets: TaskTarget[], fn: (t: TaskTarget)=>void) {
+ for (const t of targets) {
+ fn(t);
+ }
+}
+export function map(targets: TaskTarget[], fn: (t: TaskTarget)=>TaskTarget) {
+ const newTargets = [];
+ for (const t of targets) {
+ newTargets.push(fn(t));
+ }
+ return newTargets;
+}
+export function cd(targets: TaskTarget[], path: string): TaskTarget[] {
+ return targets.map(t => t.clone().cd(path));
+}
+export function glob(targets: TaskTarget[], globPath: string): TaskTarget[] {
+ return targets.map(t => t.glob(globPath)).flat();
+}
+export async function unzip(targets: TaskTarget[]): Promise {
+ return Promise.all(targets.map(t => t.unzip()));
+}
+export function read(targets: TaskTarget[]): TaskTarget[] {
+ return targets.map(t => t.clone().read())
+}
+export function cmd(targets: TaskTarget[], cmd: ValidCmd): TaskTarget[] {
+ return targets.map(t => t.clone().cmd(cmd))
+}
+export function setId(targets: TaskTarget[], id: ValidId): TaskTarget[] {
+ return targets.map(t => t.clone().setId(id))
+}
+
+/**Verify, anything that fails is skipped and throws an error*/
+export async function verify(targets: TaskTarget[]) {
+ const outTargets: TaskTarget[] = [];
+ for (const t of targets) {
+ // Make sure fsImpl is ready
+ if ("ready" in t.fsImpl && !t.fsImpl.ready && t.fsImpl.init) {
+ await t.fsImpl.init();
+ }
+ // TODO: Probably remove or assert as incorrect
+ if (t.pipeline.length <= 0) {
+ continue; // Tasks with empty pipelines are no-ops, remove
+ }
+ if (!t.exists()) {
+ console.warn(`Missing target ${t.path}`);
+ continue;
+ }
+
+ outTargets.push(t);
+ }
+ return outTargets;
+}
+
+/**Writes a manifest for parallel, a TSV where each record is an id + the shell to run
+ * @todo Enforce doing a verify before we output?
+ */
+export function getTSVManifest(targets: TaskTarget[]): string {
+ let out: string[] = [];
+ for (const t of targets) {
+ const shell = t.toShell();
+ out.push(`${t.id}\t${shell}`);
+ }
+
+ return out.join("\n");
+}
+
+function collectionSwap(a: TaskTargetPipelineHelper, b: TaskTargetPipelineHelper) {
+ if (!a.__collection) {
+ return;
+ }
+
+ // Remove a, add b
+ const collection = a.__collection;
+ delete a.__collection;
+ collection.delete(a);
+ b.__collection = collection;
+ collection.add(b);
+}
+
+export class TaskTargetPipelineHelper extends Array {
+ __collection?: Set;
+
+ static pipeline(t: TaskTarget[]): TaskTargetPipelineHelper {
+ if (Object.getPrototypeOf(t) === TaskTargetPipelineHelper.prototype) {
+ return t as any; // Already done
+ }
+ Object.setPrototypeOf(t, TaskTargetPipelineHelper.prototype);
+ return t as any;
+ }
+
+ _fn(fn: (t: TaskTarget[])=>TaskTarget[]): TaskTargetPipelineHelper {
+ const p = TaskTargetPipelineHelper.pipeline(this);
+ const t = fn(p);
+ const p2 = TaskTargetPipelineHelper.pipeline(t);
+ collectionSwap(p, p2); // Move collection pointer to the new item, ends always end up in the collection
+ return p2;
+ }
+ async _afn(fn: (t: TaskTarget[])=>Promise): Promise {
+ const p = TaskTargetPipelineHelper.pipeline(this);
+ const t = await fn(p);
+ const p2 = TaskTargetPipelineHelper.pipeline(t);
+ collectionSwap(p, p2); // Move collection pointer to the new item, ends always end up in the collection
+ return p2;
+ }
+
+ cd(path: string): TaskTargetPipelineHelper {
+ return this._fn(t => cd(t, path));
+ }
+ glob(globPath: string): TaskTargetPipelineHelper {
+ return this._fn(t => glob(t, globPath));
+ }
+ async unzip(): Promise {
+ return this._afn(unzip);
+ }
+ read(): TaskTargetPipelineHelper {
+ return this._fn(read);
+ }
+ cmd(_cmd: ValidCmd): TaskTargetPipelineHelper {
+ return this._fn(t => cmd(t, _cmd));
+ }
+ setId(id: ValidId): TaskTargetPipelineHelper {
+ return this._fn(t => setId(t, id));
+ }
+
+ types(...args: any[]) {
+ // TODO: no-op
+ return this;
+ }
+ csvSink(...args: any[]) {
+ // TODO: no-op
+ return this;
+ }
+ /**
+ * @todo Nested versions of this don't currently work, but they could if we
+ * turn __collection into an array of collections
+ */
+ collect(_c: Set) {
+ this.__collection = _c;
+ return this;
+ }
+}
+
+export async function parallel(targets: TaskTarget[]) {
+ const finalTargets = await verify(targets);
+ const manifestTSV = getTSVManifest(finalTargets);
+
+ try {
+ await $({ input: manifestTSV })`/usr/bin/parallel \
+ --colsep ${'\t'} \
+ --jobs 0 \
+ --linebuffer \
+ --tagstring {1} \
+ --eta \
+ --joblog out.manifest \
+ ${'bash -c {2} > OUTTEST/{1}.csv'} \
+ ::::- `; // stdin is in manifestTSV
+ }
+ catch(err: any) {
+ // I'm pretty sure status is the amount that failed?
+ if (err?.status >= 30) {
+ throw err;
+ }
+ }
+}
\ No newline at end of file
diff --git a/data-export/util.ts b/data-export/util.ts
deleted file mode 100644
index 8230d06..0000000
--- a/data-export/util.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-import { type ChildProcessWithoutNullStreams, spawn, type SpawnOptions } from "node:child_process";
-import { Duplex } from 'node:stream';
-
-/**@todo I wrote this when I was really tired with AI and google and it's proabbly
- * shitty, this needs a closer look. See notes at 2025-06-19T06:28:34
- * */
-export class ChildProcessDuplex extends Duplex {
- _pArgs: Parameters
- _p: ChildProcessWithoutNullStreams | undefined;
- stderrorIsError = true;
-
- constructor(command: string, args: readonly string[], options?: SpawnOptions) {
- super();
- // TODO: Typign
- this._pArgs = [command, args, options] as any;
- }
-
- __setupP() {
- // TODO: Make sure the args have the right stream setup...
- this._p = spawn(...this._pArgs) as ChildProcessWithoutNullStreams;
- this._p.on("close", ()=>{
- // If you don't do this, then a process that closes the streams near when
- // the process ends will somehow close properly, but will cause longer
- // running processes to not work. There is a test case for this because
- // I still dont quite understand it
- this.emit("close");
- });
- this._p.stdout.on('end', ()=>{
- this.push(null);
- });
- this._p.stdout.on('data', (chunk) => {
- const canPushMore = this.push(chunk);
- if (!canPushMore) {
- this._p!.stdout.pause();
- }
- });
-
- // Forward errors
- this._p.on('error', (error) => {
- this.emit('error', error);
- });
-
- if (this.stderrorIsError) {
- this._p.stderr.on('data', (data) => {
- this.emit('error', new Error(data));
- });
- }
- }
-
- _write(chunk: any, encoding: BufferEncoding, cb: ()=>any) {
- if (!this._p) {
- this.__setupP();
- }
-
- this._p!.stdin.write(chunk, encoding, cb);
- }
-
- _read(size: number) {
- if (!this._p) {
- this.__setupP();
- }
-
- this._p!.stdout.resume();
- }
-
- _final(cb: any) {
- if (this._p) {
- // Close stdin at the end of writing
- this._p.stdin.end();
- }
- cb();
- }
-}
\ No newline at end of file
diff --git a/data-export/zipFs.ts b/data-export/zipFs.ts
new file mode 100644
index 0000000..c54cb88
--- /dev/null
+++ b/data-export/zipFs.ts
@@ -0,0 +1,354 @@
+import { strict as assert } from "node:assert";
+import fs from "node:fs";
+import path from "node:path";
+import { Readable } from "node:stream";
+import yauzl from "yauzl";
+
+function removeDummyCwd(path: string) {
+ if (path.startsWith("/DUMMYCWD/")) {
+ // This is so we can properly call globSync with _some_ cwd
+ // and then strip it later, as a cwd of "" will use the current
+ // working directory of the process (which will matching nothing)
+ return path.slice("/DUMMYCWD/".length);
+ }
+ return path;
+}
+
+// Dirent-like class for directory entries
+class ZipDirent {
+ name: string;
+ private isDir: boolean;
+
+ constructor(name: string, isDirectory: boolean) {
+ this.name = name;
+ this.isDir = isDirectory;
+ }
+
+ isFile(): boolean {
+ return !this.isDir;
+ }
+
+ isDirectory(): boolean {
+ return this.isDir;
+ }
+
+ isBlockDevice(): boolean {
+ return false;
+ }
+
+ isCharacterDevice(): boolean {
+ return false;
+ }
+
+ isSymbolicLink(): boolean {
+ return false;
+ }
+
+ isFIFO(): boolean {
+ return false;
+ }
+
+ isSocket(): boolean {
+ return false;
+ }
+}
+
+function _entryToStats(entry: yauzl.Entry) {
+ const isDir = entry.fileName.endsWith("/");
+ const modDate = entry.getLastModDate();
+ return {
+ isFile: () => !isDir,
+ isDirectory: () => isDir,
+ isBlockDevice: () => false,
+ isCharacterDevice: () => false,
+ isSymbolicLink: () => false,
+ isFIFO: () => false,
+ isSocket: () => false,
+ size: entry.uncompressedSize,
+ compressedSize: entry.compressedSize,
+ mtime: modDate,
+ mode: isDir ? 0o040755 : 0o100644,
+ uid: 0,
+ gid: 0,
+ dev: 0,
+ ino: 0,
+ nlink: 1,
+ rdev: 0,
+ blksize: 4096,
+ blocks: Math.ceil(entry.uncompressedSize / 512),
+ atime: modDate,
+ ctime: modDate,
+ birthtime: modDate,
+ atimeMs: modDate.getTime(),
+ mtimeMs: modDate.getTime(),
+ ctimeMs: modDate.getTime(),
+ birthtimeMs: modDate.getTime(),
+ };
+}
+
+export class ZipFS {
+ isZip = true;
+
+ zipPath: string;
+ entries: Map;
+ zipFile: yauzl.ZipFile | null;
+
+ constructor(path: string) {
+ this.zipPath = path;
+ this.entries = new Map();
+ this.zipFile = null;
+ }
+
+ async init() {
+ this.zipFile = await new Promise((resolve, reject) => {
+ yauzl.open(this.zipPath, {
+ lazyEntries: true,
+ autoClose: false
+ }, (err, zipfile) => {
+ if (err || !zipfile) return reject(err);
+ resolve(zipfile);
+ });
+ });
+
+ await new Promise((resolve, reject) => {
+ this.zipFile!.readEntry();
+ this.zipFile!.on("entry", (entry) => {
+ const name = entry.fileName;
+ this.entries.set(name, entry);
+ this.zipFile!.readEntry();
+ });
+ this.zipFile!.on("end", resolve);
+ this.zipFile!.on("error", reject);
+ });
+ }
+ get ready() {
+ return !!this.zipFile;
+ }
+
+ existsSync(path: string): boolean {
+ assert(this.zipFile, 'Must be inited');
+ path = removeDummyCwd(path);
+ return this.entries.has(path);
+ }
+
+ stat(path: string) {
+ assert(this.zipFile, 'Must be inited');
+ path = removeDummyCwd(path);
+ const entry = this.entries.get(path);
+ if (!entry) throw new Error(`ENOENT: no such file or directory, stat '${path}'`);
+ return _entryToStats(entry);
+ }
+
+ statSync(path: string) {
+ assert(this.zipFile, 'Must be inited');
+ path = removeDummyCwd(path);
+ const entry = this.entries.get(path);
+ if (!entry) throw new Error(`ENOENT: no such file or directory, stat '${path}'`);
+ return _entryToStats(entry);
+ }
+
+ lstatSync(path: string) {
+ // ZIP files don't have symlinks, so lstat is the same as stat
+ path = removeDummyCwd(path);
+ return this.statSync(path);
+ }
+
+ createWriteStream(path: string): never {
+ throw new Error("ZIP filesystem is read-only");
+ }
+
+ createReadStream(path: string): Readable {
+ assert(this.zipFile, 'Must be inited');
+ path = removeDummyCwd(path);
+ const entry = this.entries.get(path);
+ if (!entry) throw new Error(`ENOENT: no such file or directory, open '${path}'`);
+
+
+ const out = new Readable({ read() {} });
+
+ this.zipFile.openReadStream(entry, (err, stream) => {
+ if (err || !stream) {
+ out.destroy(err ?? new Error("Failed to open stream"));
+ return;
+ }
+ stream.on("data", (chunk) => out.push(chunk));
+ stream.on("end", () => out.push(null));
+ stream.on("error", (e) => out.destroy(e));
+ });
+
+ return out;
+ }
+
+ private _listDirectory(dirPath: string): string[] {
+ // Normalize the directory path
+ let normalizedDir = dirPath.replace(/\\/g, "/");
+ if (normalizedDir && !normalizedDir.endsWith("/")) {
+ normalizedDir += "/";
+ }
+
+ const results = new Set();
+
+ for (const entryPath of this.entries.keys()) {
+ // Check if this entry is directly under the directory
+ if (entryPath === normalizedDir) continue; // Skip the directory itself
+
+ if (normalizedDir === "" || normalizedDir === "/") {
+ // Root directory - get top-level entries
+ const parts = entryPath.split("/").filter(p => p);
+ if (parts.length > 0) {
+ results.add(parts[0]);
+ }
+ } else if (entryPath.startsWith(normalizedDir)) {
+ // Get the relative path from the directory
+ const relativePath = entryPath.substring(normalizedDir.length);
+ const parts = relativePath.split("/").filter(p => p);
+ if (parts.length > 0) {
+ results.add(parts[0]);
+ }
+ }
+ }
+
+ return Array.from(results).sort();
+ }
+
+ readdirSync(dirPath: string, options?: { withFileTypes?: false }): string[];
+ readdirSync(dirPath: string, options: { withFileTypes: true }): ZipDirent[];
+ readdirSync(dirPath: string, options?: { withFileTypes?: boolean }): string[] | ZipDirent[] {
+ assert(this.zipFile, 'Must be inited');
+ dirPath = removeDummyCwd(dirPath);
+ const entries = this._listDirectory(dirPath);
+
+ if (options?.withFileTypes) {
+ return entries.map(name => {
+ let fullPath = dirPath.replace(/\\/g, "/");
+ if (fullPath && !fullPath.endsWith("/")) {
+ fullPath += "/";
+ }
+ const entryPath = fullPath + name;
+
+ // Check if it's a directory by looking for entries with this prefix
+ const isDirectory = this.entries.has(entryPath + "/") ||
+ Array.from(this.entries.keys()).some(p => p.startsWith(entryPath + "/"));
+
+ return new ZipDirent(name, isDirectory);
+ });
+ }
+
+ return entries;
+ }
+
+ async readdir(dirPath: string, options?: { withFileTypes?: false }): Promise;
+ async readdir(dirPath: string, options: { withFileTypes: true }): Promise;
+ async readdir(dirPath: string, options?: { withFileTypes?: boolean }): Promise {
+ assert(this.zipFile, 'Must be inited');
+ dirPath = removeDummyCwd(dirPath);
+ return this.readdirSync(dirPath, options as any);
+ }
+
+ readlinkSync(path: string): string {
+ // ZIP files don't support symlinks
+ throw new Error(`EINVAL: invalid argument, readlink '${path}'`);
+ }
+
+ realpathSync(path: string): string {
+ assert(this.zipFile, 'Must be inited');
+ path = removeDummyCwd(path);
+ // Normalize the path and check if it exists
+ const normalized = path.replace(/\\/g, "/");
+ if (this.entries.has(normalized) || this.entries.has(normalized + "/")) {
+ return normalized;
+ }
+
+ // Check if it's a valid directory path
+ const withSlash = normalized.endsWith("/") ? normalized : normalized + "/";
+ const hasChildren = Array.from(this.entries.keys()).some(p => p.startsWith(withSlash));
+ if (hasChildren) {
+ return normalized;
+ }
+
+ throw new Error(`ENOENT: no such file or directory, realpath '${path}'`);
+ }
+
+ promises = {
+ lstat: async (path: string) => {
+ return this.lstatSync(path);
+ },
+
+ readdir: async (dirPath: string, options?: { withFileTypes?: false }): Promise => {
+ return this.readdirSync(dirPath, options);
+ },
+
+ readlink: async (path: string): Promise => {
+ return this.readlinkSync(path);
+ },
+
+ realpath: async (path: string): Promise => {
+ return this.realpathSync(path);
+ },
+ };
+
+ lstat(path: string, callback: (err: Error | null, stats?: any) => void) {
+ assert(this.zipFile, 'Must be inited');
+ path = removeDummyCwd(path);
+ try {
+ const stats = this.lstatSync(path);
+ callback(null, stats);
+ } catch (err) {
+ callback(err as Error);
+ }
+ }
+
+ realpath(path: string, callback: (err: Error | null, resolvedPath?: string) => void) {
+ assert(this.zipFile, 'Must be inited');
+ path = removeDummyCwd(path);
+ try {
+ const resolved = this.realpathSync(path);
+ callback(null, resolved);
+ } catch (err) {
+ callback(err as Error);
+ }
+ }
+
+ getImpl() {
+ // Because glob uses ...xxx notation to unpack ourselves into a _new_ object
+ // we need to make sure that we DONT use a class, otherwise the properties
+ // will be non-enumerable and not show up in the output object
+ return {
+ isZip: this.isZip,
+ zipPath: this.zipPath,
+ init: this.init.bind(this),
+ ready: this.ready,
+
+ statSync: this.statSync.bind(this),
+ createReadStream: this.createReadStream.bind(this),
+ createWriteStream: this.createWriteStream.bind(this),
+ existsSync: this.existsSync.bind(this),
+
+ lstatSync: this.lstatSync.bind(this),
+ readdir: this.readdir.bind(this),
+ readdirSync: this.readdirSync.bind(this),
+ readlinkSync: this.readlinkSync.bind(this),
+ realpathSync: this.realpathSync.bind(this),
+ };
+ }
+
+ access() { throw new Error("Not implemented"); }
+ appendFile() { throw new Error("Not implemented"); }
+ chmod() { throw new Error("Not implemented"); }
+ chown() { throw new Error("Not implemented"); }
+ copyFile() { throw new Error("Not implemented"); }
+ mkdir() { throw new Error("Not implemented"); }
+ mkdtemp() { throw new Error("Not implemented"); }
+ open() { throw new Error("Not implemented"); }
+ readFile() { throw new Error("Not implemented"); }
+ rename() { throw new Error("Not implemented"); }
+ rm() { throw new Error("Not implemented"); }
+ rmdir() { throw new Error("Not implemented"); }
+ statfs() { throw new Error("Not implemented"); }
+ symlink() { throw new Error("Not implemented"); }
+ truncate() { throw new Error("Not implemented"); }
+ unlink() { throw new Error("Not implemented"); }
+ utimes() { throw new Error("Not implemented"); }
+ watch() { throw new Error("Not implemented"); }
+ writeFile() { throw new Error("Not implemented"); }
+}
diff --git a/datasets/example-dataset/README.md b/datasets/example-dataset/README.md
deleted file mode 100644
index 7e43fe0..0000000
--- a/datasets/example-dataset/README.md
+++ /dev/null
@@ -1,28 +0,0 @@
-# Example Dataset
-
-This is an example dataset to demonstrate the Base Data Manager prototype.
-
-## Overview
-
-This dataset contains sample data for testing purposes.
-
-## Data Source
-
-- **Source**: Example data generator
-- **Last Updated**: 2024
-- **Format**: CSV
-
-## Fields
-
-- `id`: Unique identifier
-- `name`: Name field
-- `value`: Numeric value
-- `timestamp`: Date/time of entry
-
-## Usage
-
-Import this dataset using the `data-import.ts` script.
-
-## Notes
-
-This is just example data for testing the prototype functionality.
\ No newline at end of file
diff --git a/datasets/example-dataset/data-export.ts b/datasets/example-dataset/data-export.ts
deleted file mode 100644
index 19fe851..0000000
--- a/datasets/example-dataset/data-export.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-export async function main() {
- // Simulate some data processing
- console.log('Processing example dataset...');
-
- // Simulate async operation
- await new Promise(resolve => setTimeout(resolve, 100));
-
- // Return stats
- return {
- rows: 42
- };
-}
\ No newline at end of file
diff --git a/index.html b/index.html
deleted file mode 100644
index b5bdba7..0000000
--- a/index.html
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
-
-
- Base Data Manager
-
-
-
-
-
-
-
diff --git a/main.ts b/main.ts
new file mode 100644
index 0000000..a8e234a
--- /dev/null
+++ b/main.ts
@@ -0,0 +1,67 @@
+import fs from 'node:fs/promises';
+import nodePath from "node:path";
+import { DatabaseSync } from "node:sqlite";
+import "./data-export/facebook.ts";
+import { google } from "./data-export/google.ts";
+import { TaskTargetPipelineHelper } from "./data-export/task.ts";
+
+declare module "./data-export/task.ts" {
+ interface TaskTargetPipelineHelper {
+ google: typeof google;
+ }
+}
+
+Object.assign(TaskTargetPipelineHelper.prototype, {
+ google
+});
+
+function loadIntoSqlite(
+ paths: string[],
+ sqlitePath: string
+) {
+ // Open an in-memory db for speed
+ const db = new DatabaseSync(":memory:", { allowExtension: true });
+ db.loadExtension("/home/cobertos/sqlite-files/csv.so")
+ db.enableLoadExtension(false);
+ for (const path of paths) {
+ const table = nodePath.basename(path, ".csv");
+ console.log(`Loading ${path} → table ${table}`);
+
+ // const headers = lines[0].split(",");
+ // const columnsSql = headers.map(h => `"${h}" TEXT`).join(", ");
+ db.exec(`CREATE VIRTUAL TABLE temp.intermediate USING csv(filename='${path}');`);
+ db.exec(`CREATE TABLE "${table}" AS SELECT * FROM intermediate;`);
+ db.exec(`DROP TABLE IF EXISTS intermediate;`);
+ }
+
+ // Dump it all to the path specified
+ db.exec(`VACUUM main INTO '${sqlitePath}'`);
+ db.close();
+}
+
+async function main() {
+ const t = TaskTargetPipelineHelper;
+ // TODO:
+ // t.fork().cd("/home/cobertos/Seafile/archive/ExportedServiceData/facebook/formapcast_facebook-DEADNAME-May2021-json")
+ // .facebook()
+
+ // (await t.fork().cd("/home/cobertos/Seafile/archive/ExportedServiceData/facebook/facebook-x-2025-11-29-x.zip").zip()).facebook_v2();
+
+ // t.fork().cd("/home/cobertos/Seafile/archive/ExportedServiceData/google/2023-NAMEwork-001")
+ // .google()
+
+
+ // let zipTask = t.fork().zip("/home/cobertos/Seafile/archive/ExportedServiceData/facebook/facebook-DEADNAME-May2021-json.zip");
+ // await (zipTask.fsImpl as any).init();
+
+ // zipTask.facebook();
+ // Now take the output and load it all into a single SQLITE file
+ // const entries = await fs.readdir('OUTTEST', { withFileTypes: true });
+ // const csvFiles = entries
+ // .filter(e => e.isFile() && e.name.endsWith(".csv"))
+ // .map(e => nodePath.join('OUTTEST', e.name));
+ // await fs.unlink('your.db');
+ // loadIntoSqlite(csvFiles, 'your.db');
+}
+
+main();
\ No newline at end of file
diff --git a/package.json b/package.json
index c95dff9..b16957c 100644
--- a/package.json
+++ b/package.json
@@ -15,22 +15,17 @@
"license": "ISC",
"packageManager": "pnpm@10.19.0",
"dependencies": {
- "@preact/signals": "^2.2.1",
- "@types/cors": "^2.8.19",
- "@types/express": "^5.0.5",
- "cors": "^2.8.5",
- "dotenv": "^17.2.3",
- "esbuild": "^0.27.0",
- "express": "^5.1.0",
- "preact": "^10.26.9",
- "preact-custom-element": "^4.3.0",
- "preact-render-to-string": "^6.6.3"
+ "@types/duplexify": "^3.6.5",
+ "@types/yauzl": "^2.10.3",
+ "duplexify": "^4.1.3",
+ "fp-ts": "^2.16.11",
+ "glob": "^13.0.0",
+ "htmlparser2": "^10.0.0",
+ "yauzl": "^3.2.0",
+ "zx": "^8.8.5"
},
"devDependencies": {
- "@types/jsdom": "^21.1.7",
"@types/node": "^24.1.0",
- "jsdom": "^26.1.0",
- "typescript": "^5.9.3",
- "vite": "^7.0.6"
+ "typescript": "^5.9.3"
}
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index ae2fa7f..d654bac 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -11,24 +11,48 @@ importers:
'@preact/signals':
specifier: ^2.2.1
version: 2.4.0(preact@10.27.2)
+ '@types/cli-progress':
+ specifier: ^3.11.6
+ version: 3.11.6
'@types/cors':
specifier: ^2.8.19
version: 2.8.19
+ '@types/duplexify':
+ specifier: ^3.6.5
+ version: 3.6.5
'@types/express':
specifier: ^5.0.5
version: 5.0.5
+ '@types/progress-stream':
+ specifier: ^2.0.5
+ version: 2.0.5
+ '@types/yauzl':
+ specifier: ^2.10.3
+ version: 2.10.3
cors:
specifier: ^2.8.5
version: 2.8.5
dotenv:
specifier: ^17.2.3
version: 17.2.3
+ duplexify:
+ specifier: ^4.1.3
+ version: 4.1.3
esbuild:
specifier: ^0.27.0
version: 0.27.0
express:
specifier: ^5.1.0
version: 5.1.0
+ fp-ts:
+ specifier: ^2.16.11
+ version: 2.16.11
+ glob:
+ specifier: ^13.0.0
+ version: 13.0.0
+ htmlparser2:
+ specifier: ^10.0.0
+ version: 10.0.0
preact:
specifier: ^10.26.9
version: 10.27.2
@@ -38,6 +62,12 @@ importers:
preact-render-to-string:
specifier: ^6.6.3
version: 6.6.3(preact@10.27.2)
+ yauzl:
+ specifier: ^3.2.0
+ version: 3.2.0
+ zx:
+ specifier: ^8.8.5
+ version: 8.8.5
devDependencies:
'@types/jsdom':
specifier: ^21.1.7
@@ -400,6 +430,14 @@ packages:
cpu: [x64]
os: [win32]
+ '@isaacs/balanced-match@4.0.1':
+ resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==}
+ engines: {node: 20 || >=22}
+
+ '@isaacs/brace-expansion@5.0.0':
+ resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==}
+ engines: {node: 20 || >=22}
+
'@preact/signals-core@1.12.1':
resolution: {integrity: sha512-BwbTXpj+9QutoZLQvbttRg5x3l5468qaV2kufh+51yha1c53ep5dY4kTuZR35+3pAZxpfQerGJiQqg34ZNZ6uA==}
@@ -521,12 +559,18 @@ packages:
'@types/body-parser@1.19.6':
resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==}
+ '@types/cli-progress@3.11.6':
+ resolution: {integrity: sha512-cE3+jb9WRlu+uOSAugewNpITJDt1VF8dHOopPO4IABFc3SXYL5WE/+PTz/FCdZRRfIujiWW3n3aMbv1eIGVRWA==}
+
'@types/connect@3.4.38':
resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
'@types/cors@2.8.19':
resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==}
+ '@types/duplexify@3.6.5':
+ resolution: {integrity: sha512-fB56ACzlW91UdZ5F3VXplVMDngO8QaX5Y2mjvADtN01TT2TMy4WjF0Lg+tFDvt4uMBeTe4SgaD+qCrA7dL5/tA==}
+
'@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
@@ -548,6 +592,9 @@ packages:
'@types/node@24.10.0':
resolution: {integrity: sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==}
+ '@types/progress-stream@2.0.5':
+ resolution: {integrity: sha512-5YNriuEZkHlFHHepLIaxzq3atGeav1qCTGzB74HKWpo66qjfostF+rHc785YYYHeBytve8ZG3ejg42jEIfXNiQ==}
+
'@types/qs@6.14.0':
resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==}
@@ -566,6 +613,9 @@ packages:
'@types/tough-cookie@4.0.5':
resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==}
+ '@types/yauzl@2.10.3':
+ resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==}
+
accepts@2.0.0:
resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==}
engines: {node: '>= 0.6'}
@@ -578,6 +628,9 @@ packages:
resolution: {integrity: sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==}
engines: {node: '>=18'}
+ buffer-crc32@0.2.13:
+ resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
+
bytes@3.1.2:
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
engines: {node: '>= 0.8'}
@@ -634,6 +687,19 @@ packages:
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
engines: {node: '>= 0.8'}
+ dom-serializer@2.0.0:
+ resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
+
+ domelementtype@2.3.0:
+ resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
+
+ domhandler@5.0.3:
+ resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
+ engines: {node: '>= 4'}
+
+ domutils@3.2.2:
+ resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
+
dotenv@17.2.3:
resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==}
engines: {node: '>=12'}
@@ -642,6 +708,9 @@ packages:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
+ duplexify@4.1.3:
+ resolution: {integrity: sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==}
+
ee-first@1.1.1:
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
@@ -649,6 +718,13 @@ packages:
resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
engines: {node: '>= 0.8'}
+ end-of-stream@1.4.5:
+ resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==}
+
+ entities@4.5.0:
+ resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
+ engines: {node: '>=0.12'}
+
entities@6.0.1:
resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
engines: {node: '>=0.12'}
@@ -703,6 +779,9 @@ packages:
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
engines: {node: '>= 0.6'}
+ fp-ts@2.16.11:
+ resolution: {integrity: sha512-LaI+KaX2NFkfn1ZGHoKCmcfv7yrZsC3b8NtWsTVQeHkq4F27vI5igUuO53sxqDEa2gNQMHFPmpojDw/1zmUK7w==}
+
fresh@2.0.0:
resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==}
engines: {node: '>= 0.8'}
@@ -723,6 +802,10 @@ packages:
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
engines: {node: '>= 0.4'}
+ glob@13.0.0:
+ resolution: {integrity: sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==}
+ engines: {node: 20 || >=22}
+
gopd@1.2.0:
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
engines: {node: '>= 0.4'}
@@ -739,6 +822,9 @@ packages:
resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==}
engines: {node: '>=18'}
+ htmlparser2@10.0.0:
+ resolution: {integrity: sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==}
+
http-errors@2.0.1:
resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==}
engines: {node: '>= 0.8'}
@@ -784,6 +870,10 @@ packages:
lru-cache@10.4.3:
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
+ lru-cache@11.2.2:
+ resolution: {integrity: sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==}
+ engines: {node: 20 || >=22}
+
math-intrinsics@1.1.0:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
@@ -804,6 +894,14 @@ packages:
resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==}
engines: {node: '>=18'}
+ minimatch@10.1.1:
+ resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==}
+ engines: {node: 20 || >=22}
+
+ minipass@7.1.2:
+ resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
+ engines: {node: '>=16 || 14 >=14.17'}
+
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
@@ -841,9 +939,16 @@ packages:
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
engines: {node: '>= 0.8'}
+ path-scurry@2.0.1:
+ resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==}
+ engines: {node: 20 || >=22}
+
path-to-regexp@8.3.0:
resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==}
+ pend@1.2.0:
+ resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==}
+
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
@@ -888,6 +993,10 @@ packages:
resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==}
engines: {node: '>= 0.10'}
+ readable-stream@3.6.2:
+ resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
+ engines: {node: '>= 6'}
+
rollup@4.52.5:
resolution: {integrity: sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
@@ -900,6 +1009,9 @@ packages:
rrweb-cssom@0.8.0:
resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==}
+ safe-buffer@5.1.2:
+ resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
+
safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
@@ -942,6 +1054,12 @@ packages:
resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
engines: {node: '>= 0.8'}
+ stream-shift@1.0.3:
+ resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==}
+
+ string_decoder@1.1.1:
+ resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
+
symbol-tree@3.2.4:
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
@@ -984,6 +1102,9 @@ packages:
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
engines: {node: '>= 0.8'}
+ util-deprecate@1.0.2:
+ resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
+
vary@1.1.2:
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
engines: {node: '>= 0.8'}
@@ -1070,6 +1191,15 @@ packages:
xmlchars@2.2.0:
resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
+ yauzl@3.2.0:
+ resolution: {integrity: sha512-Ow9nuGZE+qp1u4JIPvg+uCiUr7xGQWdff7JQSk5VGYTAZMDe2q8lxJ10ygv10qmSj031Ty/6FNJpLO4o1Sgc+w==}
+ engines: {node: '>=12'}
+
+ zx@8.8.5:
+ resolution: {integrity: sha512-SNgDF5L0gfN7FwVOdEFguY3orU5AkfFZm9B5YSHog/UDHv+lvmd82ZAsOenOkQixigwH2+yyH198AwNdKhj+RA==}
+ engines: {node: '>= 12.17.0'}
+ hasBin: true
+
snapshots:
'@asamuzakjp/css-color@3.2.0':
@@ -1256,6 +1386,12 @@ snapshots:
'@esbuild/win32-x64@0.27.0':
optional: true
+ '@isaacs/balanced-match@4.0.1': {}
+
+ '@isaacs/brace-expansion@5.0.0':
+ dependencies:
+ '@isaacs/balanced-match': 4.0.1
+
'@preact/signals-core@1.12.1': {}
'@preact/signals@2.4.0(preact@10.27.2)':
@@ -1334,6 +1470,10 @@ snapshots:
'@types/connect': 3.4.38
'@types/node': 24.10.0
+ '@types/cli-progress@3.11.6':
+ dependencies:
+ '@types/node': 24.10.0
+
'@types/connect@3.4.38':
dependencies:
'@types/node': 24.10.0
@@ -1342,6 +1482,10 @@ snapshots:
dependencies:
'@types/node': 24.10.0
+ '@types/duplexify@3.6.5':
+ dependencies:
+ '@types/node': 24.10.0
+
'@types/estree@1.0.8': {}
'@types/express-serve-static-core@5.1.0':
@@ -1371,6 +1515,10 @@ snapshots:
dependencies:
undici-types: 7.16.0
+ '@types/progress-stream@2.0.5':
+ dependencies:
+ '@types/node': 24.10.0
+
'@types/qs@6.14.0': {}
'@types/range-parser@1.2.7': {}
@@ -1392,6 +1540,10 @@ snapshots:
'@types/tough-cookie@4.0.5': {}
+ '@types/yauzl@2.10.3':
+ dependencies:
+ '@types/node': 24.10.0
+
accepts@2.0.0:
dependencies:
mime-types: 3.0.2
@@ -1413,6 +1565,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ buffer-crc32@0.2.13: {}
+
bytes@3.1.2: {}
call-bind-apply-helpers@1.0.2:
@@ -1456,6 +1610,24 @@ snapshots:
depd@2.0.0: {}
+ dom-serializer@2.0.0:
+ dependencies:
+ domelementtype: 2.3.0
+ domhandler: 5.0.3
+ entities: 4.5.0
+
+ domelementtype@2.3.0: {}
+
+ domhandler@5.0.3:
+ dependencies:
+ domelementtype: 2.3.0
+
+ domutils@3.2.2:
+ dependencies:
+ dom-serializer: 2.0.0
+ domelementtype: 2.3.0
+ domhandler: 5.0.3
+
dotenv@17.2.3: {}
dunder-proto@1.0.1:
@@ -1464,10 +1636,23 @@ snapshots:
es-errors: 1.3.0
gopd: 1.2.0
+ duplexify@4.1.3:
+ dependencies:
+ end-of-stream: 1.4.5
+ inherits: 2.0.4
+ readable-stream: 3.6.2
+ stream-shift: 1.0.3
+
ee-first@1.1.1: {}
encodeurl@2.0.0: {}
+ end-of-stream@1.4.5:
+ dependencies:
+ once: 1.4.0
+
+ entities@4.5.0: {}
+
entities@6.0.1: {}
es-define-property@1.0.1: {}
@@ -1589,6 +1774,8 @@ snapshots:
forwarded@0.2.0: {}
+ fp-ts@2.16.11: {}
+
fresh@2.0.0: {}
fsevents@2.3.3:
@@ -1614,6 +1801,12 @@ snapshots:
dunder-proto: 1.0.1
es-object-atoms: 1.1.1
+ glob@13.0.0:
+ dependencies:
+ minimatch: 10.1.1
+ minipass: 7.1.2
+ path-scurry: 2.0.1
+
gopd@1.2.0: {}
has-symbols@1.1.0: {}
@@ -1626,6 +1819,13 @@ snapshots:
dependencies:
whatwg-encoding: 3.1.1
+ htmlparser2@10.0.0:
+ dependencies:
+ domelementtype: 2.3.0
+ domhandler: 5.0.3
+ domutils: 3.2.2
+ entities: 6.0.1
+
http-errors@2.0.1:
dependencies:
depd: 2.0.0
@@ -1693,6 +1893,8 @@ snapshots:
lru-cache@10.4.3: {}
+ lru-cache@11.2.2: {}
+
math-intrinsics@1.1.0: {}
media-typer@1.1.0: {}
@@ -1705,6 +1907,12 @@ snapshots:
dependencies:
mime-db: 1.54.0
+ minimatch@10.1.1:
+ dependencies:
+ '@isaacs/brace-expansion': 5.0.0
+
+ minipass@7.1.2: {}
+
ms@2.1.3: {}
nanoid@3.3.11: {}
@@ -1731,8 +1939,15 @@ snapshots:
parseurl@1.3.3: {}
+ path-scurry@2.0.1:
+ dependencies:
+ lru-cache: 11.2.2
+ minipass: 7.1.2
+
path-to-regexp@8.3.0: {}
+ pend@1.2.0: {}
+
picocolors@1.1.1: {}
picomatch@4.0.3: {}
@@ -1773,6 +1988,12 @@ snapshots:
iconv-lite: 0.7.0
unpipe: 1.0.0
+ readable-stream@3.6.2:
+ dependencies:
+ inherits: 2.0.4
+ string_decoder: 1.1.1
+ util-deprecate: 1.0.2
+
rollup@4.52.5:
dependencies:
'@types/estree': 1.0.8
@@ -1813,6 +2034,8 @@ snapshots:
rrweb-cssom@0.8.0: {}
+ safe-buffer@5.1.2: {}
+
safer-buffer@2.1.2: {}
saxes@6.0.0:
@@ -1878,6 +2101,12 @@ snapshots:
statuses@2.0.2: {}
+ stream-shift@1.0.3: {}
+
+ string_decoder@1.1.1:
+ dependencies:
+ safe-buffer: 5.1.2
+
symbol-tree@3.2.4: {}
tinyglobby@0.2.15:
@@ -1913,6 +2142,8 @@ snapshots:
unpipe@1.0.0: {}
+ util-deprecate@1.0.2: {}
+
vary@1.1.2: {}
vite@7.1.12(@types/node@24.10.0):
@@ -1951,3 +2182,10 @@ snapshots:
xml-name-validator@5.0.0: {}
xmlchars@2.2.0: {}
+
+ yauzl@3.2.0:
+ dependencies:
+ buffer-crc32: 0.2.13
+ pend: 1.2.0
+
+ zx@8.8.5: {}
diff --git a/server/prototype.ts b/server/prototype.ts
deleted file mode 100644
index 7efd220..0000000
--- a/server/prototype.ts
+++ /dev/null
@@ -1,152 +0,0 @@
-import { readdir, readFile, writeFile } from 'fs/promises';
-import { join, resolve } from 'path';
-import { h } from 'preact';
-import render from 'preact-render-to-string';
-import DatasetList from '../src/DatasetList.tsx';
-
-// Get SEARCH_ROOT_PATH from argv or environment variable
-const SEARCH_ROOT_PATH = process.argv[2] || process.env.SEARCH_ROOT_PATH;
-
-if (!SEARCH_ROOT_PATH) {
- throw new Error('SEARCH_ROOT_PATH must be provided either as first argument or as environment variable');
-}
-
-interface Stats {
- rows: number;
-}
-
-interface DataImportModule {
- main(): Promise;
-}
-
-interface Dataset {
- name: string;
- readme: string;
- stats: Stats | null;
- error?: string;
-}
-
-async function readReadme(folderPath: string): Promise {
- try {
- const readmePath = join(folderPath, 'README.md');
- const content = await readFile(readmePath, 'utf-8');
- return content;
- } catch (error) {
- return '';
- }
-}
-
-async function importDataStats(folderPath: string): Promise<{ stats: Stats | null; error?: string }> {
- try {
- const dataImportPath = join(folderPath, 'data-import.ts');
- const absolutePath = resolve(dataImportPath);
-
- // Dynamically import the data-import.ts module
- const module = await import(absolutePath) as DataImportModule;
-
- if (typeof module.main !== 'function') {
- return { stats: null, error: 'No main() function found in data-import.ts' };
- }
-
- const stats = await module.main();
- return { stats, error: undefined };
- } catch (error) {
- const errorMessage = error instanceof Error ? error.message : String(error);
- return { stats: null, error: errorMessage };
- }
-}
-
-// The script expects the following structure in the `SEARCH_ROOT_PATH` (defaults to `./datasets`):
-// ```
-// datasets/
-// ├── dataset-1/
-// │ ├── README.md
-// │ └── data-import.ts
-// ├── dataset-2/
-// │ ├── README.md
-// │ └── data-import.ts
-// └── dataset-3/
-// ├── README.md
-// └── data-import.ts
-// ```
-
-async function scanDatasets(rootPath: string): Promise {
- const datasets: Dataset[] = [];
-
- try {
- const entries = await readdir(rootPath, { withFileTypes: true });
-
- for (const entry of entries) {
- if (entry.isDirectory()) {
- const folderPath = join(rootPath, entry.name);
-
- console.log(`Processing dataset: ${entry.name}`);
-
- // Read README.md
- const readme = await readReadme(folderPath);
-
- // Import and run data-import.ts
- const { stats, error } = await importDataStats(folderPath);
-
- datasets.push({
- name: entry.name,
- readme,
- stats,
- error
- });
- }
- }
- } catch (error) {
- console.error(`Error scanning directory ${rootPath}:`, error);
- }
-
- return datasets;
-}
-
-function generateHTML(htmlContent: string): string {
- return `
-
-
-
-
- Base Data Manager
-
-
-
- ${htmlContent}
-
-`;
-}
-
-async function main() {
- console.log(`Scanning datasets in: ${SEARCH_ROOT_PATH}`);
-
- // Scan all datasets
- const datasets = await scanDatasets(SEARCH_ROOT_PATH);
-
- console.log(`Found ${datasets.length} dataset(s)`);
-
- // Render Preact component to string
- const appHtml = render(h(DatasetList, { datasets }));
-
- // Generate complete HTML document
- const fullHtml = generateHTML(appHtml);
-
- // Write to dashboard.html in current working directory
- const outputPath = join(process.cwd(), 'dashboard.html');
- await writeFile(outputPath, fullHtml, 'utf-8');
-
- console.log(`Generated HTML written to: ${outputPath}`);
-}
-
-main().catch(console.error);
diff --git a/server/prototype2.ts b/server/prototype2.ts
deleted file mode 100644
index e8d2f90..0000000
--- a/server/prototype2.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { facebook } from "../data-export/facebook2.ts";
-import { Task } from "../data-export/task.ts";
-
-declare module "../data-export/task.ts" {
- interface Task {
- facebook: typeof facebook;
- }
-}
-
-Object.assign(Task.prototype, {
- facebook
-});
-
-async function main() {
- const t = new Task();
- t.facebook("/home/cobertos/Seafile/archive/ExportedServiceData/facebook/formapcast_facebook-DEADNAME-May2021-json");
- await t.doTasks();
-}
-
-main();
\ No newline at end of file
diff --git a/server/server.ts b/server/server.ts
deleted file mode 100644
index acfed45..0000000
--- a/server/server.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-import { strict as assert } from 'node:assert';
-import nodePath from 'node:path';
-import { fileURLToPath } from "node:url";
-import fs from 'node:fs';
-import { type IncomingMessage } from 'node:http';
-import http from 'node:http';
-import { type Socket } from "node:net";
-import express from 'express';
-import cors from 'cors';
-import dotenv from "dotenv";
-dotenv.config();
-const __filename = fileURLToPath(import.meta.url);
-const __dirname = nodePath.dirname(__filename);
-
-/**Gets the express app with all the endpoints*/
-function getExpressApp() {
- const app = express();
- app.use(cors());
- app.use(express.json());
- // if (serveUI) {
- // const compiler = webpack(webpackConfig);
- // app.use(middleware(compiler));
- // //app.use(express.static(nodePath.join(__dirname, appFilesDir)));
- // }
- app.get('/', (req: express.Request, res: express.Response) => {
- res.send('Server!');
- });
- app.get('/info', (req: express.Request, res: express.Response) => {
- const infoPayload = JSON.stringify({
- test: 'test'
- });
- res.setHeader('Content-Type', 'text/json');
- res.status(200);
- res.send(infoPayload);
- });
-
- return app;
-}
-
-async function ready(server: http.Server) {
- await new Promise((resolve, reject)=> {
- server.on('listening', resolve);
- server.on('error', reject);
- });
-}
-
-export async function makeServer() {
- // Get the servers
- const app = getExpressApp();
- let httpServer: http.Server;
- httpServer = http.createServer(app);
-
- // Listen to the available servers
- const addr = '127.0.0.1';
- httpServer.listen({ port: '2947', hostname: addr });
-
- // Wait for everything to be ready
- await ready(httpServer);
- console.log(`Running HTTP server on ${(httpServer.address() as any).address}:${(httpServer.address() as any).port}...`);
-}
-
-if (process.argv[1] === fileURLToPath(import.meta.url)) {
- makeServer();
-}
diff --git a/src/DatasetItem.tsx b/src/DatasetItem.tsx
deleted file mode 100644
index 6db631f..0000000
--- a/src/DatasetItem.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-import { h } from 'preact';
-
-interface DatasetItemProps {
- name: string;
- readme: string;
- stats: {
- rows: number;
- } | null;
- error?: string;
-}
-
-export default function DatasetItem({ name, readme, stats, error }: DatasetItemProps) {
- return (
-
-
- {name}
-
-
-
- Stats
- {error ? (
- Error loading data: {error}
- ) : stats ? (
-
- Rows: {stats.rows}
-
- ) : (
- No stats available
- )}
-
-
- README
-
-{readme || 'No README.md found'}
-
-
-
-
- );
-}
\ No newline at end of file
diff --git a/src/DatasetList.tsx b/src/DatasetList.tsx
deleted file mode 100644
index 80cc55e..0000000
--- a/src/DatasetList.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import { h } from 'preact';
-import DatasetItem from './DatasetItem.tsx';
-
-interface Stats {
- rows: number;
-}
-
-interface Dataset {
- name: string;
- readme: string;
- stats: Stats | null;
- error?: string;
-}
-
-interface DatasetListProps {
- datasets: Dataset[];
-}
-
-export default function DatasetList({ datasets }: DatasetListProps) {
- return (
-
- Base Data Manager
- Found {datasets.length} dataset(s)
-
- {datasets.map((dataset) => (
-
- ))}
-
-
- );
-}
\ No newline at end of file
diff --git a/src/MainUI.tsx b/src/MainUI.tsx
deleted file mode 100644
index 1600eaf..0000000
--- a/src/MainUI.tsx
+++ /dev/null
@@ -1,8 +0,0 @@
-import { h, render } from 'preact';
-import { signal } from '@preact/signals';
-
-export default function MainUI() {
- return <>
- test
- >
-}
diff --git a/src/main.ts b/src/main.ts
deleted file mode 100644
index 3f8fdd6..0000000
--- a/src/main.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { h, render } from 'preact';
-import MainUI from "./MainUI.tsx";
-
-export default function main() {
- const component = h(MainUI, {});
- const outputBefore = render(component, document.body);
-}
diff --git a/test/facebook.ts b/test/facebook.ts
new file mode 100644
index 0000000..44c2ba2
--- /dev/null
+++ b/test/facebook.ts
@@ -0,0 +1,115 @@
+import test from "node:test";
+import fs from "node:fs";
+import assert from "node:assert";
+import { finished } from "node:stream/promises";
+import { Readable, Writable } from "node:stream";
+import { TaskTargetPipelineHelper } from "../data-export/task.ts";
+
+test("facebook: Can load the 2021 export", async () => {
+ // TODO:
+ // const t = new Task();
+ // (await t.fork().cd("/home/cobertos/Seafile/archive/ExportedServiceData/facebook/facebook-x-2025-11-29-x.zip").zip()).facebook_v2();
+ // const taskText = await t.getTaskTSVShell();
+ // await fs.writeFile('test.manifest', taskText);
+ // // Run everything with parallel
+ // try {
+ // execFileSync('/usr/bin/parallel', ['--colsep', '\t', '--jobs', '0', '--linebuffer', '--tagstring', '{1}', '--eta', '--joblog', 'out.manifest', 'bash -c {2} > OUTTEST/{1}.csv', '::::', 'test.manifest'], {
+ // stdio: 'inherit'
+ // });
+ // }
+ // catch(err: any) {
+ // // I'm pretty sure status is the amount that failed?
+ // if (err?.status >= 30) {
+ // throw err;
+ // }
+ // }
+
+ // // Now take the output and load it all into a single SQLITE file
+ // const entries = await fs.readdir('OUTTEST', { withFileTypes: true });
+ // const csvFiles = entries
+ // .filter(e => e.isFile() && e.name.endsWith(".csv"))
+ // .map(e => nodePath.join('OUTTEST', e.name));
+});
+
+// import fs from 'node:fs/promises';
+// import { type SpawnOptions, execFile as _execFile, execFileSync } from "node:child_process";
+// import nodePath from "node:path";
+// import { DatabaseSync } from "node:sqlite";
+// import { promisify } from "node:util";
+// import "../data-export/facebook.ts";
+// import { google } from "../data-export/google.ts";
+// const execFile = promisify(_execFile);
+
+// declare module "../data-export/task.ts" {
+// interface Task {
+// google: typeof google;
+// }
+// }
+
+// Object.assign(Task.prototype, {
+// google
+// });
+
+// function loadIntoSqlite(
+// paths: string[],
+// sqlitePath: string
+// ) {
+// // Open an in-memory db for speed
+// const db = new DatabaseSync(":memory:", { allowExtension: true });
+// db.loadExtension("/home/cobertos/sqlite-files/csv.so")
+// db.enableLoadExtension(false);
+// for (const path of paths) {
+// const table = nodePath.basename(path, ".csv");
+// console.log(`Loading ${path} → table ${table}`);
+
+// // const headers = lines[0].split(",");
+// // const columnsSql = headers.map(h => `"${h}" TEXT`).join(", ");
+// db.exec(`CREATE VIRTUAL TABLE temp.intermediate USING csv(filename='${path}');`);
+// db.exec(`CREATE TABLE "${table}" AS SELECT * FROM intermediate;`);
+// db.exec(`DROP TABLE IF EXISTS intermediate;`);
+// }
+
+// // Dump it all to the path specified
+// db.exec(`VACUUM main INTO '${sqlitePath}'`);
+// db.close();
+// }
+
+// async function main() {
+// const t = new Task();
+// // t.fork().cd("/home/cobertos/Seafile/archive/ExportedServiceData/facebook/formapcast_facebook-DEADNAME-May2021-json")
+// // .facebook()
+
+// (await t.fork().cd("/home/cobertos/Seafile/archive/ExportedServiceData/facebook/facebook-x-2025-11-29-x.zip").zip()).facebook_v2();
+
+// // t.fork().cd("/home/cobertos/Seafile/archive/ExportedServiceData/google/2023-NAMEwork-001")
+// // .google()
+
+
+// // let zipTask = t.fork().zip("/home/cobertos/Seafile/archive/ExportedServiceData/facebook/facebook-DEADNAME-May2021-json.zip");
+// // await (zipTask.fsImpl as any).init();
+
+// // zipTask.facebook();
+// const taskText = await t.getTaskTSVShell();
+// await fs.writeFile('test.manifest', taskText);
+// // Run everything with parallel
+// try {
+// execFileSync('/usr/bin/parallel', ['--colsep', '\t', '--jobs', '0', '--linebuffer', '--tagstring', '{1}', '--eta', '--joblog', 'out.manifest', 'bash -c {2} > OUTTEST/{1}.csv', '::::', 'test.manifest'], {
+// stdio: 'inherit'
+// });
+// }
+// catch(err: any) {
+// // I'm pretty sure status is the amount that failed?
+// if (err?.status >= 30) {
+// throw err;
+// }
+// }
+// // Now take the output and load it all into a single SQLITE file
+// const entries = await fs.readdir('OUTTEST', { withFileTypes: true });
+// const csvFiles = entries
+// .filter(e => e.isFile() && e.name.endsWith(".csv"))
+// .map(e => nodePath.join('OUTTEST', e.name));
+// await fs.unlink('your.db');
+// loadIntoSqlite(csvFiles, 'your.db');
+// }
+
+// main();
\ No newline at end of file
diff --git a/test/fixtures/README.md b/test/fixtures/README.md
new file mode 100644
index 0000000..75fd3a9
--- /dev/null
+++ b/test/fixtures/README.md
@@ -0,0 +1,13 @@
+### Addition process
+
+* Paste in a new folder
+* Manually scan through _folders_ and delete any long lists of files. For example, facebook chats, we dont need 100 chat thread .json files
+* Run `util/scrub.ts` inside that folder
+* Fix any warnings that come out of the above for manual edits
+* Manually blank out any file/folder names with IDs or PII
+* Manually scan through each file for any remaining PII
+
+### Corpus
+
+* `facebook-json-2021-05-01` - Facebook JSON export
+* `facebook-json-2025-11-29` - Facebook JSON export
diff --git a/test/fixtures/facebook-json-2021-05-01/about_you/face_recognition.json b/test/fixtures/facebook-json-2021-05-01/about_you/face_recognition.json
new file mode 100644
index 0000000..6ccc709
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/about_you/face_recognition.json
@@ -0,0 +1,9 @@
+{
+ "facial_data": {
+ "threshold_green": 69,
+ "threshold_yellow": 69,
+ "threshold_red": 69,
+ "example_count": 69,
+ "raw_data": "xxx"
+ }
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/about_you/friend_peer_group.json b/test/fixtures/facebook-json-2021-05-01/about_you/friend_peer_group.json
new file mode 100644
index 0000000..68c4479
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/about_you/friend_peer_group.json
@@ -0,0 +1,3 @@
+{
+ "friend_peer_group": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/about_you/messenger.json b/test/fixtures/facebook-json-2021-05-01/about_you/messenger.json
new file mode 100644
index 0000000..24daa44
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/about_you/messenger.json
@@ -0,0 +1,42 @@
+{
+ "messenger": {
+ "secret_conversations": {
+ "has_sent_message": false,
+ "has_received_message": true,
+ "tincan_devices": []
+ },
+ "autofill_information": {
+ "CITY": [
+ "xxx"
+ ],
+ "STATE": [
+ "xxx"
+ ],
+ "COUNTRY": [
+ "xxx"
+ ],
+ "EMAIL": [
+ "not_a_real_email@example.com",
+ "not_a_real_email@example.com"
+ ],
+ "JOB_TITLE": [
+ "xxx"
+ ],
+ "COMPANY_NAME": [
+ "xxx"
+ ],
+ "GENDER": [
+ "xxx"
+ ],
+ "FIRST_NAME": [
+ "xxx"
+ ],
+ "LAST_NAME": [
+ "xxx"
+ ],
+ "FULL_NAME": [
+ "xxx"
+ ]
+ }
+ }
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/about_you/notifications.json b/test/fixtures/facebook-json-2021-05-01/about_you/notifications.json
new file mode 100644
index 0000000..2e21303
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/about_you/notifications.json
@@ -0,0 +1,16 @@
+{
+ "notifications": [
+ {
+ "timestamp": 1714465000,
+ "unread": true,
+ "href": "url://somewhere",
+ "text": "xxx"
+ },
+ {
+ "timestamp": 1714465000,
+ "unread": true,
+ "href": "url://somewhere",
+ "text": "xxx"
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/about_you/preferences.json b/test/fixtures/facebook-json-2021-05-01/about_you/preferences.json
new file mode 100644
index 0000000..b9cd47e
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/about_you/preferences.json
@@ -0,0 +1,37 @@
+{
+ "preferences": [
+ {
+ "name": "xxx",
+ "description": "xxx",
+ "entries": [
+ {
+ "timestamp": 1704500000,
+ "data": {
+ "name": "xxx",
+ "uri": "url://somewhere"
+ }
+ }
+ ]
+ },
+ {
+ "name": "xxx",
+ "description": "xxx",
+ "entries": [
+ {
+ "timestamp": 1708835000,
+ "data": {
+ "name": "xxx",
+ "uri": "url://somewhere"
+ }
+ },
+ {
+ "timestamp": 1713130000,
+ "data": {
+ "name": "xxx",
+ "uri": "url://somewhere"
+ }
+ }
+ ]
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/about_you/viewed.json b/test/fixtures/facebook-json-2021-05-01/about_you/viewed.json
new file mode 100644
index 0000000..959aab4
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/about_you/viewed.json
@@ -0,0 +1,72 @@
+{
+ "viewed_things": [
+ {
+ "name": "xxx",
+ "description": "xxx",
+ "children": [
+ {
+ "name": "xxx",
+ "description": "xxx",
+ "entries": [
+ {
+ "timestamp": 1728855000,
+ "data": {
+ "name": "xxx",
+ "uri": "url://somewhere",
+ "watch_time": "xxx"
+ }
+ },
+ {
+ "timestamp": 1721720000,
+ "data": {
+ "name": "xxx",
+ "uri": "url://somewhere",
+ "watch_time": "xxx"
+ }
+ }
+ ]
+ },
+ {
+ "name": "xxx",
+ "description": "xxx",
+ "entries": [
+ {
+ "timestamp": 1723345000,
+ "data": {
+ "uri": "url://somewhere",
+ "name": "xxx"
+ }
+ },
+ {
+ "timestamp": 1730165000,
+ "data": {
+ "uri": "url://somewhere",
+ "name": "xxx"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "xxx",
+ "description": "xxx",
+ "entries": [
+ {
+ "timestamp": 1714200000,
+ "data": {
+ "name": "xxx",
+ "uri": "url://somewhere"
+ }
+ },
+ {
+ "timestamp": 1707070000,
+ "data": {
+ "name": "xxx",
+ "uri": "url://somewhere"
+ }
+ }
+ ]
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/about_you/visited.json b/test/fixtures/facebook-json-2021-05-01/about_you/visited.json
new file mode 100644
index 0000000..dfe1b23
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/about_you/visited.json
@@ -0,0 +1,44 @@
+{
+ "visited_things": [
+ {
+ "name": "xxx",
+ "description": "xxx",
+ "entries": [
+ {
+ "timestamp": 1731145000,
+ "data": {
+ "name": "xxx",
+ "uri": "url://somewhere"
+ }
+ },
+ {
+ "timestamp": 1731145000,
+ "data": {
+ "name": "xxx",
+ "uri": "url://somewhere"
+ }
+ }
+ ]
+ },
+ {
+ "name": "xxx",
+ "description": "xxx",
+ "entries": [
+ {
+ "timestamp": 1706175000,
+ "data": {
+ "name": "xxx",
+ "uri": "url://somewhere"
+ }
+ },
+ {
+ "timestamp": 1704250000,
+ "data": {
+ "name": "xxx",
+ "uri": "url://somewhere"
+ }
+ }
+ ]
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/accounts_center/accounts_and_profiles.json b/test/fixtures/facebook-json-2021-05-01/accounts_center/accounts_and_profiles.json
new file mode 100644
index 0000000..03457db
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/accounts_center/accounts_and_profiles.json
@@ -0,0 +1,19 @@
+{
+ "linked_accounts": [
+ {
+ "service_name": "xxx",
+ "native_app_id": 69,
+ "username": "xxx",
+ "email": "not_a_real_email@example.com",
+ "phone_number": "xxx",
+ "name": "xxx"
+ },
+ {
+ "service_name": "xxx",
+ "native_app_id": 1707005000,
+ "username": "xxx",
+ "email": "not_a_real_email@example.com",
+ "name": "xxx"
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/ads_and_businesses/advertisers_who_uploaded_a_contact_list_with_your_information.json b/test/fixtures/facebook-json-2021-05-01/ads_and_businesses/advertisers_who_uploaded_a_contact_list_with_your_information.json
new file mode 100644
index 0000000..9e15cf9
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/ads_and_businesses/advertisers_who_uploaded_a_contact_list_with_your_information.json
@@ -0,0 +1,6 @@
+{
+ "custom_audiences": [
+ "xxx",
+ "xxx"
+ ]
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/ads_and_businesses/your_off-facebook_activity.json b/test/fixtures/facebook-json-2021-05-01/ads_and_businesses/your_off-facebook_activity.json
new file mode 100644
index 0000000..2cfd0f7
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/ads_and_businesses/your_off-facebook_activity.json
@@ -0,0 +1,34 @@
+{
+ "off_facebook_activity": [
+ {
+ "name": "xxx",
+ "events": [
+ {
+ "id": 69,
+ "type": "xxx",
+ "timestamp": 1707655000
+ },
+ {
+ "id": 69,
+ "type": "xxx",
+ "timestamp": 1707595000
+ }
+ ]
+ },
+ {
+ "name": "xxx",
+ "events": [
+ {
+ "id": 69,
+ "type": "xxx",
+ "timestamp": 1707565000
+ },
+ {
+ "id": 69,
+ "type": "xxx",
+ "timestamp": 1707340000
+ }
+ ]
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/apps_and_websites/apps_and_websites.json b/test/fixtures/facebook-json-2021-05-01/apps_and_websites/apps_and_websites.json
new file mode 100644
index 0000000..c9c54d4
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/apps_and_websites/apps_and_websites.json
@@ -0,0 +1,12 @@
+{
+ "installed_apps": [
+ {
+ "name": "xxx",
+ "added_timestamp": 1735460000
+ },
+ {
+ "name": "xxx",
+ "added_timestamp": 1725280000
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/archive/no-data.txt b/test/fixtures/facebook-json-2021-05-01/archive/no-data.txt
new file mode 100644
index 0000000..25340eb
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/archive/no-data.txt
@@ -0,0 +1 @@
+You have no data in this section
\ No newline at end of file
diff --git a/test/fixtures/facebook-json-2021-05-01/campus/no-data.txt b/test/fixtures/facebook-json-2021-05-01/campus/no-data.txt
new file mode 100644
index 0000000..25340eb
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/campus/no-data.txt
@@ -0,0 +1 @@
+You have no data in this section
\ No newline at end of file
diff --git a/test/fixtures/facebook-json-2021-05-01/comments/comments.json b/test/fixtures/facebook-json-2021-05-01/comments/comments.json
new file mode 100644
index 0000000..f48fcc0
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/comments/comments.json
@@ -0,0 +1,32 @@
+{
+ "comments": [
+ {
+ "timestamp": 1707420000,
+ "data": [
+ {
+ "comment": {
+ "timestamp": 1707245000,
+ "comment": "xxx",
+ "author": "xxx",
+ "group": "xxx"
+ }
+ }
+ ],
+ "title": "xxx"
+ },
+ {
+ "timestamp": 1705500000,
+ "data": [
+ {
+ "comment": {
+ "timestamp": 1705320000,
+ "comment": "xxx",
+ "author": "xxx",
+ "group": "xxx"
+ }
+ }
+ ],
+ "title": "xxx"
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/dating/communities.json b/test/fixtures/facebook-json-2021-05-01/dating/communities.json
new file mode 100644
index 0000000..e6feb99
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/dating/communities.json
@@ -0,0 +1,12 @@
+{
+ "dating_communities": {
+ "events": [
+ "xxx",
+ "xxx"
+ ],
+ "groups": [
+ "xxx",
+ "xxx"
+ ]
+ }
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/dating/messages/0.json b/test/fixtures/facebook-json-2021-05-01/dating/messages/0.json
new file mode 100644
index 0000000..e65b89a
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/dating/messages/0.json
@@ -0,0 +1,13 @@
+{
+ "recipient": "xxx",
+ "messages": [
+ {
+ "timestamp": 1705130000,
+ "body": "xxx"
+ },
+ {
+ "timestamp": 1705130000,
+ "body": "xxx"
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/dating/your_dating_activity.json b/test/fixtures/facebook-json-2021-05-01/dating/your_dating_activity.json
new file mode 100644
index 0000000..a10a5fe
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/dating/your_dating_activity.json
@@ -0,0 +1,7 @@
+{
+ "dating_your_activity": {
+ "recently_active_date_range": "xxx",
+ "liked_count": 69,
+ "passed_count": 69
+ }
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/dating/your_dating_preferences.json b/test/fixtures/facebook-json-2021-05-01/dating/your_dating_preferences.json
new file mode 100644
index 0000000..1c2afe5
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/dating/your_dating_preferences.json
@@ -0,0 +1,15 @@
+{
+ "dating_preferences": {
+ "min_age": 69,
+ "max_age": 69,
+ "children": "xxx",
+ "distance": 69,
+ "gender": [
+ "xxx",
+ "xxx"
+ ],
+ "religious_views": [
+ "xxx"
+ ]
+ }
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/dating/your_dating_profile.json b/test/fixtures/facebook-json-2021-05-01/dating/your_dating_profile.json
new file mode 100644
index 0000000..46c6baa
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/dating/your_dating_profile.json
@@ -0,0 +1,94 @@
+{
+ "dating_profile": {
+ "additional_locations": [],
+ "blocked_people": [
+ "xxx"
+ ],
+ "children": "xxx",
+ "college": [
+ "xxx"
+ ],
+ "current_city": "xxx",
+ "company": [
+ "xxx"
+ ],
+ "profile_questions": [
+ {
+ "question": "xxx",
+ "answer": "xxx"
+ },
+ {
+ "question": "xxx",
+ "answer": "xxx"
+ }
+ ],
+ "gender": [
+ "xxx"
+ ],
+ "grad_school": [],
+ "height": 69,
+ "high_school": [
+ "xxx"
+ ],
+ "hobbies": [
+ "xxx",
+ "xxx"
+ ],
+ "hometown": "xxx",
+ "intro": "url://somewhere",
+ "job_title": [
+ "xxx"
+ ],
+ "profile_picture": {
+ "uri": "photos_and_videos/SomeAlbum_xxxxxxx/200x200png.png",
+ "creation_timestamp": 1705555000,
+ "media_metadata": {
+ "photo_metadata": {
+ "exif_data": [
+ {
+ "upload_ip": "1.1.1.1",
+ "taken_timestamp": 1705465000
+ }
+ ]
+ }
+ },
+ "title": "xxx"
+ },
+ "religious_views": [
+ "xxx"
+ ],
+ "photos": [
+ {
+ "uri": "photos_and_videos/SomeAlbum_xxxxxxx/200x200png.png",
+ "creation_timestamp": 1705470000,
+ "media_metadata": {
+ "photo_metadata": {
+ "exif_data": [
+ {
+ "upload_ip": "1.1.1.1",
+ "taken_timestamp": 1705295000
+ }
+ ]
+ }
+ },
+ "title": "xxx"
+ },
+ {
+ "uri": "photos_and_videos/SomeAlbum_xxxxxxx/200x200png.png",
+ "creation_timestamp": 1732045000,
+ "media_metadata": {
+ "photo_metadata": {
+ "exif_data": [
+ {
+ "upload_ip": "1.1.1.1",
+ "taken_timestamp": 1731870000
+ }
+ ]
+ }
+ },
+ "title": "xxx"
+ }
+ ],
+ "account_status": "xxx"
+ }
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/events/event_invitations.json b/test/fixtures/facebook-json-2021-05-01/events/event_invitations.json
new file mode 100644
index 0000000..6c2c066
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/events/event_invitations.json
@@ -0,0 +1,14 @@
+{
+ "events_invited": [
+ {
+ "name": "xxx",
+ "start_timestamp": 1706525000,
+ "end_timestamp": 1706540000
+ },
+ {
+ "name": "xxx",
+ "start_timestamp": 1734470000,
+ "end_timestamp": 1734485000
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/events/your_event_responses.json b/test/fixtures/facebook-json-2021-05-01/events/your_event_responses.json
new file mode 100644
index 0000000..281be4d
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/events/your_event_responses.json
@@ -0,0 +1,40 @@
+{
+ "event_responses": {
+ "events_joined": [
+ {
+ "name": "xxx",
+ "start_timestamp": 1729040000,
+ "end_timestamp": 1729055000
+ },
+ {
+ "name": "xxx",
+ "start_timestamp": 1728395000,
+ "end_timestamp": 1728435000
+ }
+ ],
+ "events_declined": [
+ {
+ "name": "xxx",
+ "start_timestamp": 1719505000,
+ "end_timestamp": 69
+ },
+ {
+ "name": "xxx",
+ "start_timestamp": 1722705000,
+ "end_timestamp": 1722730000
+ }
+ ],
+ "events_interested": [
+ {
+ "name": "xxx",
+ "start_timestamp": 1706715000,
+ "end_timestamp": 69
+ },
+ {
+ "name": "xxx",
+ "start_timestamp": 1704645000,
+ "end_timestamp": 1704835000
+ }
+ ]
+ }
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/facebook_gaming/instant_games.json b/test/fixtures/facebook-json-2021-05-01/facebook_gaming/instant_games.json
new file mode 100644
index 0000000..6cf8894
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/facebook_gaming/instant_games.json
@@ -0,0 +1,8 @@
+{
+ "instant_games_played": [
+ {
+ "name": "xxx",
+ "added_timestamp": 1730650000
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/following_and_followers/followers.json b/test/fixtures/facebook-json-2021-05-01/following_and_followers/followers.json
new file mode 100644
index 0000000..6cdb219
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/following_and_followers/followers.json
@@ -0,0 +1,10 @@
+{
+ "followers": [
+ {
+ "name": "xxx"
+ },
+ {
+ "name": "xxx"
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/following_and_followers/following.json b/test/fixtures/facebook-json-2021-05-01/following_and_followers/following.json
new file mode 100644
index 0000000..35b311f
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/following_and_followers/following.json
@@ -0,0 +1,12 @@
+{
+ "following": [
+ {
+ "name": "xxx",
+ "timestamp": 1714550000
+ },
+ {
+ "name": "xxx",
+ "timestamp": 1714550000
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/following_and_followers/unfollowed_pages.json b/test/fixtures/facebook-json-2021-05-01/following_and_followers/unfollowed_pages.json
new file mode 100644
index 0000000..8c77240
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/following_and_followers/unfollowed_pages.json
@@ -0,0 +1,13 @@
+{
+ "pages_unfollowed": [
+ {
+ "timestamp": 1734425000,
+ "data": [
+ {
+ "name": "xxx"
+ }
+ ],
+ "title": "xxx"
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/friends/friends.json b/test/fixtures/facebook-json-2021-05-01/friends/friends.json
new file mode 100644
index 0000000..881aec5
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/friends/friends.json
@@ -0,0 +1,12 @@
+{
+ "friends": [
+ {
+ "name": "xxx",
+ "timestamp": 1707830000
+ },
+ {
+ "name": "xxx",
+ "timestamp": 1730335000
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/friends/received_friend_requests.json b/test/fixtures/facebook-json-2021-05-01/friends/received_friend_requests.json
new file mode 100644
index 0000000..5d567d1
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/friends/received_friend_requests.json
@@ -0,0 +1,12 @@
+{
+ "received_requests": [
+ {
+ "name": "xxx",
+ "timestamp": 1707410000
+ },
+ {
+ "name": "xxx",
+ "timestamp": 1727205000
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/friends/rejected_friend_requests.json b/test/fixtures/facebook-json-2021-05-01/friends/rejected_friend_requests.json
new file mode 100644
index 0000000..776736d
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/friends/rejected_friend_requests.json
@@ -0,0 +1,12 @@
+{
+ "rejected_requests": [
+ {
+ "name": "xxx",
+ "timestamp": 1727450000
+ },
+ {
+ "name": "xxx",
+ "timestamp": 1724460000
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/friends/removed_friends.json b/test/fixtures/facebook-json-2021-05-01/friends/removed_friends.json
new file mode 100644
index 0000000..377be7f
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/friends/removed_friends.json
@@ -0,0 +1,12 @@
+{
+ "deleted_friends": [
+ {
+ "name": "xxx",
+ "timestamp": 1705215000
+ },
+ {
+ "name": "xxx",
+ "timestamp": 1705215000
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/friends/sent_friend_requests.json b/test/fixtures/facebook-json-2021-05-01/friends/sent_friend_requests.json
new file mode 100644
index 0000000..2fb6240
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/friends/sent_friend_requests.json
@@ -0,0 +1,12 @@
+{
+ "sent_requests": [
+ {
+ "name": "xxx",
+ "timestamp": 1719120000
+ },
+ {
+ "name": "xxx",
+ "timestamp": 1716625000
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/groups/your_group_membership_activity.json b/test/fixtures/facebook-json-2021-05-01/groups/your_group_membership_activity.json
new file mode 100644
index 0000000..53b69f9
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/groups/your_group_membership_activity.json
@@ -0,0 +1,12 @@
+{
+ "groups_joined": [
+ {
+ "timestamp": 1705215000,
+ "title": "xxx"
+ },
+ {
+ "timestamp": 1705215000,
+ "title": "xxx"
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/groups/your_posts_and_comments_in_groups.json b/test/fixtures/facebook-json-2021-05-01/groups/your_posts_and_comments_in_groups.json
new file mode 100644
index 0000000..bd197e9
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/groups/your_posts_and_comments_in_groups.json
@@ -0,0 +1,34 @@
+{
+ "group_posts": {
+ "activity_log_data": [
+ {
+ "timestamp": 1707420000,
+ "data": [
+ {
+ "comment": {
+ "timestamp": 1707245000,
+ "comment": "xxx",
+ "author": "xxx",
+ "group": "xxx"
+ }
+ }
+ ],
+ "title": "xxx"
+ },
+ {
+ "timestamp": 1707420000,
+ "data": [
+ {
+ "comment": {
+ "timestamp": 1707245000,
+ "comment": "xxx",
+ "author": "xxx",
+ "group": "xxx"
+ }
+ }
+ ],
+ "title": "xxx"
+ }
+ ]
+ }
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/interactions/events.json b/test/fixtures/facebook-json-2021-05-01/interactions/events.json
new file mode 100644
index 0000000..72f13ef
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/interactions/events.json
@@ -0,0 +1,16 @@
+{
+ "events_interactions": [
+ {
+ "name": "xxx",
+ "description": "xxx",
+ "entries": [
+ {
+ "timestamp": 1705040000,
+ "data": {
+ "value": "xxx"
+ }
+ }
+ ]
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/interactions/groups.json b/test/fixtures/facebook-json-2021-05-01/interactions/groups.json
new file mode 100644
index 0000000..ea24171
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/interactions/groups.json
@@ -0,0 +1,24 @@
+{
+ "group_interactions": [
+ {
+ "name": "xxx",
+ "description": "xxx",
+ "entries": [
+ {
+ "data": {
+ "name": "xxx",
+ "value": "xxx",
+ "uri": "url://somewhere"
+ }
+ },
+ {
+ "data": {
+ "name": "xxx",
+ "value": "xxx",
+ "uri": "url://somewhere"
+ }
+ }
+ ]
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/interactions/people.json b/test/fixtures/facebook-json-2021-05-01/interactions/people.json
new file mode 100644
index 0000000..333b34d
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/interactions/people.json
@@ -0,0 +1,24 @@
+{
+ "people_interactions": [
+ {
+ "name": "xxx",
+ "description": "xxx",
+ "entries": [
+ {
+ "timestamp": 1705320000,
+ "data": {
+ "name": "xxx",
+ "uri": "url://somewhere"
+ }
+ },
+ {
+ "timestamp": 1705040000,
+ "data": {
+ "name": "xxx",
+ "uri": "url://somewhere"
+ }
+ }
+ ]
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/journalist_registration/no-data.txt b/test/fixtures/facebook-json-2021-05-01/journalist_registration/no-data.txt
new file mode 100644
index 0000000..25340eb
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/journalist_registration/no-data.txt
@@ -0,0 +1 @@
+You have no data in this section
\ No newline at end of file
diff --git a/test/fixtures/facebook-json-2021-05-01/likes_and_reactions/pages.json b/test/fixtures/facebook-json-2021-05-01/likes_and_reactions/pages.json
new file mode 100644
index 0000000..db0009d
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/likes_and_reactions/pages.json
@@ -0,0 +1,12 @@
+{
+ "page_likes": [
+ {
+ "name": "xxx",
+ "timestamp": 1714550000
+ },
+ {
+ "name": "xxx",
+ "timestamp": 1714550000
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/likes_and_reactions/posts_and_comments.json b/test/fixtures/facebook-json-2021-05-01/likes_and_reactions/posts_and_comments.json
new file mode 100644
index 0000000..11bffb6
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/likes_and_reactions/posts_and_comments.json
@@ -0,0 +1,28 @@
+{
+ "reactions": [
+ {
+ "timestamp": 1705215000,
+ "data": [
+ {
+ "reaction": {
+ "reaction": "xxx",
+ "actor": "xxx"
+ }
+ }
+ ],
+ "title": "xxx"
+ },
+ {
+ "timestamp": 1705215000,
+ "data": [
+ {
+ "reaction": {
+ "reaction": "xxx",
+ "actor": "xxx"
+ }
+ }
+ ],
+ "title": "xxx"
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/location/primary_location.json b/test/fixtures/facebook-json-2021-05-01/location/primary_location.json
new file mode 100644
index 0000000..a945be0
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/location/primary_location.json
@@ -0,0 +1,13 @@
+{
+ "primary_location": {
+ "city_region_pairs": [
+ [
+ "xxx",
+ "xxx"
+ ]
+ ],
+ "zipcode": [
+ "xxx"
+ ]
+ }
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/location/primary_public_location.json b/test/fixtures/facebook-json-2021-05-01/location/primary_public_location.json
new file mode 100644
index 0000000..1f8877d
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/location/primary_public_location.json
@@ -0,0 +1,7 @@
+{
+ "primary_public_location": {
+ "city": "xxx",
+ "region": "xxx",
+ "country": "xxx"
+ }
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/location/timezone.json b/test/fixtures/facebook-json-2021-05-01/location/timezone.json
new file mode 100644
index 0000000..f74111f
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/location/timezone.json
@@ -0,0 +1,3 @@
+{
+ "timezone": "some/path"
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/marketplace/items_sold.json b/test/fixtures/facebook-json-2021-05-01/marketplace/items_sold.json
new file mode 100644
index 0000000..95c730f
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/marketplace/items_sold.json
@@ -0,0 +1,36 @@
+{
+ "items_selling": [
+ {
+ "title": "xxx",
+ "price": "xxx",
+ "seller": "xxx",
+ "created_timestamp": 1734500000,
+ "updated_timestamp": 1734500000,
+ "category": "xxx",
+ "marketplace": "xxx",
+ "location": {
+ "coordinate": {
+ "latitude": 69,
+ "longitude": 69
+ }
+ },
+ "description": "xxx"
+ },
+ {
+ "title": "xxx",
+ "price": "xxx",
+ "seller": "xxx",
+ "created_timestamp": 1734500000,
+ "updated_timestamp": 1734500000,
+ "category": "xxx",
+ "marketplace": "xxx",
+ "location": {
+ "coordinate": {
+ "latitude": 69,
+ "longitude": 69
+ }
+ },
+ "description": "xxx"
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/messages/archived_threads/randomuser_xxxxxxxx/message_1.json b/test/fixtures/facebook-json-2021-05-01/messages/archived_threads/randomuser_xxxxxxxx/message_1.json
new file mode 100644
index 0000000..9ddac8f
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/messages/archived_threads/randomuser_xxxxxxxx/message_1.json
@@ -0,0 +1,30 @@
+{
+ "participants": [
+ {
+ "name": "xxx"
+ },
+ {
+ "name": "xxx"
+ }
+ ],
+ "messages": [
+ {
+ "sender_name": "xxx",
+ "timestamp_ms": 69,
+ "content": "xxx",
+ "type": "xxx",
+ "is_unsent": false
+ },
+ {
+ "sender_name": "xxx",
+ "timestamp_ms": 69,
+ "content": "xxx",
+ "type": "xxx",
+ "is_unsent": false
+ }
+ ],
+ "title": "xxx",
+ "is_still_participant": true,
+ "thread_type": "xxx",
+ "thread_path": "some/path"
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/messages/inbox/randomuser2_xxxxxxxx/files/letter_xxxxx.txt b/test/fixtures/facebook-json-2021-05-01/messages/inbox/randomuser2_xxxxxxxx/files/letter_xxxxx.txt
new file mode 100644
index 0000000..9ac76ff
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/messages/inbox/randomuser2_xxxxxxxx/files/letter_xxxxx.txt
@@ -0,0 +1,2 @@
+
+xxxx
\ No newline at end of file
diff --git a/test/fixtures/facebook-json-2021-05-01/messages/inbox/randomuser2_xxxxxxxx/message_1.json b/test/fixtures/facebook-json-2021-05-01/messages/inbox/randomuser2_xxxxxxxx/message_1.json
new file mode 100644
index 0000000..9ddac8f
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/messages/inbox/randomuser2_xxxxxxxx/message_1.json
@@ -0,0 +1,30 @@
+{
+ "participants": [
+ {
+ "name": "xxx"
+ },
+ {
+ "name": "xxx"
+ }
+ ],
+ "messages": [
+ {
+ "sender_name": "xxx",
+ "timestamp_ms": 69,
+ "content": "xxx",
+ "type": "xxx",
+ "is_unsent": false
+ },
+ {
+ "sender_name": "xxx",
+ "timestamp_ms": 69,
+ "content": "xxx",
+ "type": "xxx",
+ "is_unsent": false
+ }
+ ],
+ "title": "xxx",
+ "is_still_participant": true,
+ "thread_type": "xxx",
+ "thread_path": "some/path"
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/messages/message_requests/randomuser3_xxxxxxx/message_1.json b/test/fixtures/facebook-json-2021-05-01/messages/message_requests/randomuser3_xxxxxxx/message_1.json
new file mode 100644
index 0000000..bcab4a1
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/messages/message_requests/randomuser3_xxxxxxx/message_1.json
@@ -0,0 +1,23 @@
+{
+ "participants": [
+ {
+ "name": "xxx"
+ },
+ {
+ "name": "xxx"
+ }
+ ],
+ "messages": [
+ {
+ "sender_name": "xxx",
+ "timestamp_ms": 69,
+ "content": "xxx",
+ "type": "xxx",
+ "is_unsent": false
+ }
+ ],
+ "title": "xxx",
+ "is_still_participant": true,
+ "thread_type": "xxx",
+ "thread_path": "some/path"
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/messages/message_requests/randomuser4_xxxxxxx/message_1.json b/test/fixtures/facebook-json-2021-05-01/messages/message_requests/randomuser4_xxxxxxx/message_1.json
new file mode 100644
index 0000000..bcab4a1
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/messages/message_requests/randomuser4_xxxxxxx/message_1.json
@@ -0,0 +1,23 @@
+{
+ "participants": [
+ {
+ "name": "xxx"
+ },
+ {
+ "name": "xxx"
+ }
+ ],
+ "messages": [
+ {
+ "sender_name": "xxx",
+ "timestamp_ms": 69,
+ "content": "xxx",
+ "type": "xxx",
+ "is_unsent": false
+ }
+ ],
+ "title": "xxx",
+ "is_still_participant": true,
+ "thread_type": "xxx",
+ "thread_path": "some/path"
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/music_recommendations/no-data.txt b/test/fixtures/facebook-json-2021-05-01/music_recommendations/no-data.txt
new file mode 100644
index 0000000..25340eb
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/music_recommendations/no-data.txt
@@ -0,0 +1 @@
+You have no data in this section
\ No newline at end of file
diff --git a/test/fixtures/facebook-json-2021-05-01/news/your_locations.json b/test/fixtures/facebook-json-2021-05-01/news/your_locations.json
new file mode 100644
index 0000000..93bf7ef
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/news/your_locations.json
@@ -0,0 +1,5 @@
+{
+ "news_your_locations": [
+ "xxx"
+ ]
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/other_activity/pokes.json b/test/fixtures/facebook-json-2021-05-01/other_activity/pokes.json
new file mode 100644
index 0000000..0d9715b
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/other_activity/pokes.json
@@ -0,0 +1,12 @@
+{
+ "pokes": {
+ "data": [
+ {
+ "poker": "xxx",
+ "pokee": "xxx",
+ "rank": 69,
+ "timestamp": 1721675000
+ }
+ ]
+ }
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/other_activity/support_correspondences.json b/test/fixtures/facebook-json-2021-05-01/other_activity/support_correspondences.json
new file mode 100644
index 0000000..0ae420e
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/other_activity/support_correspondences.json
@@ -0,0 +1,26 @@
+{
+ "support_correspondence": [
+ {
+ "timestamp": 1729235000,
+ "subject": "xxx",
+ "messages": [
+ {
+ "from": "not_a_real_email@example.com",
+ "to": "xxx",
+ "subject": "xxx",
+ "message": "xxx",
+ "timestamp": 1729060000,
+ "attachments": []
+ },
+ {
+ "from": "xxx",
+ "to": "xxx",
+ "subject": "xxx",
+ "message": "url://somewhere",
+ "timestamp": 1729060000,
+ "attachments": []
+ }
+ ]
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/pages/no-data.txt b/test/fixtures/facebook-json-2021-05-01/pages/no-data.txt
new file mode 100644
index 0000000..25340eb
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/pages/no-data.txt
@@ -0,0 +1 @@
+You have no data in this section
\ No newline at end of file
diff --git a/test/fixtures/facebook-json-2021-05-01/payment_history/payment_history.json b/test/fixtures/facebook-json-2021-05-01/payment_history/payment_history.json
new file mode 100644
index 0000000..a2e06e1
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/payment_history/payment_history.json
@@ -0,0 +1,17 @@
+{
+ "payments": {
+ "preferred_currency": "xxx",
+ "payments": [
+ {
+ "created_timestamp": 1714945000,
+ "amount": "xxx",
+ "currency": "xxx",
+ "sender": "xxx",
+ "receiver": "xxx",
+ "type": "xxx",
+ "status": "xxx",
+ "payment_method": "xxx"
+ }
+ ]
+ }
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/photos_and_videos/CoverPhotos_yyyyyy/200x200png.png b/test/fixtures/facebook-json-2021-05-01/photos_and_videos/CoverPhotos_yyyyyy/200x200png.png
new file mode 100644
index 0000000..f9feed8
Binary files /dev/null and b/test/fixtures/facebook-json-2021-05-01/photos_and_videos/CoverPhotos_yyyyyy/200x200png.png differ
diff --git a/test/fixtures/facebook-json-2021-05-01/photos_and_videos/SomeAlbum_xxxxxxx/200x200png.png b/test/fixtures/facebook-json-2021-05-01/photos_and_videos/SomeAlbum_xxxxxxx/200x200png.png
new file mode 100644
index 0000000..f9feed8
Binary files /dev/null and b/test/fixtures/facebook-json-2021-05-01/photos_and_videos/SomeAlbum_xxxxxxx/200x200png.png differ
diff --git a/test/fixtures/facebook-json-2021-05-01/photos_and_videos/album/0.json b/test/fixtures/facebook-json-2021-05-01/photos_and_videos/album/0.json
new file mode 100644
index 0000000..a18f723
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/photos_and_videos/album/0.json
@@ -0,0 +1,52 @@
+{
+ "name": "xxx",
+ "photos": [
+ {
+ "uri": "photos_and_videos/CoverPhotos_yyyyyy/200x200png.png",
+ "creation_timestamp": 1709825000,
+ "media_metadata": {
+ "photo_metadata": {
+ "exif_data": [
+ {
+ "upload_ip": "1.1.1.1",
+ "taken_timestamp": 1708000000
+ }
+ ]
+ }
+ },
+ "title": "xxx"
+ },
+ {
+ "uri": "photos_and_videos/CoverPhotos_yyyyyy/200x200png.png",
+ "creation_timestamp": 1719820000,
+ "media_metadata": {
+ "photo_metadata": {
+ "exif_data": [
+ {
+ "upload_ip": "1.1.1.1",
+ "taken_timestamp": 1718825000
+ }
+ ]
+ }
+ },
+ "title": "xxx"
+ }
+ ],
+ "cover_photo": {
+ "uri": "photos_and_videos/CoverPhotos_yyyyyy/200x200png.png",
+ "creation_timestamp": 1719910000,
+ "media_metadata": {
+ "photo_metadata": {
+ "exif_data": [
+ {
+ "upload_ip": "1.1.1.1",
+ "taken_timestamp": 1719005000
+ }
+ ]
+ }
+ },
+ "title": "xxx"
+ },
+ "last_modified_timestamp": 1719910000,
+ "description": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/photos_and_videos/videos/sample.mp4 b/test/fixtures/facebook-json-2021-05-01/photos_and_videos/videos/sample.mp4
new file mode 100644
index 0000000..88c4f5b
Binary files /dev/null and b/test/fixtures/facebook-json-2021-05-01/photos_and_videos/videos/sample.mp4 differ
diff --git a/test/fixtures/facebook-json-2021-05-01/photos_and_videos/your_posts/200x200png.png b/test/fixtures/facebook-json-2021-05-01/photos_and_videos/your_posts/200x200png.png
new file mode 100644
index 0000000..f9feed8
Binary files /dev/null and b/test/fixtures/facebook-json-2021-05-01/photos_and_videos/your_posts/200x200png.png differ
diff --git a/test/fixtures/facebook-json-2021-05-01/photos_and_videos/your_videos.json b/test/fixtures/facebook-json-2021-05-01/photos_and_videos/your_videos.json
new file mode 100644
index 0000000..a63af6d
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/photos_and_videos/your_videos.json
@@ -0,0 +1,23 @@
+{
+ "videos": [
+ {
+ "uri": "photos_and_videos/videos/sample.mp4",
+ "creation_timestamp": 1723410000,
+ "media_metadata": {
+ "video_metadata": {
+ "exif_data": [
+ {
+ "upload_ip": "1.1.1.1",
+ "upload_timestamp": 69
+ }
+ ]
+ }
+ },
+ "thumbnail": {
+ "uri": "photos_and_videos/videos/sample.mp4"
+ },
+ "title": "xxx",
+ "description": "xxx"
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/posts/your_pinned_posts.json b/test/fixtures/facebook-json-2021-05-01/posts/your_pinned_posts.json
new file mode 100644
index 0000000..427e7ab
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/posts/your_pinned_posts.json
@@ -0,0 +1,24 @@
+{
+ "pinned_posts": [
+ {
+ "name": "xxx",
+ "description": "xxx",
+ "entries": [
+ {
+ "timestamp": 1709010000,
+ "data": {
+ "name": "xxx",
+ "uri": "url://somewhere"
+ }
+ },
+ {
+ "timestamp": 1715830000,
+ "data": {
+ "name": "xxx",
+ "uri": "url://somewhere"
+ }
+ }
+ ]
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/posts/your_posts_1.json b/test/fixtures/facebook-json-2021-05-01/posts/your_posts_1.json
new file mode 100644
index 0000000..2067bbc
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/posts/your_posts_1.json
@@ -0,0 +1,54 @@
+[
+ {
+ "timestamp": 1714550000,
+ "data": [
+ {
+ "post": "xxx"
+ }
+ ],
+ "title": "xxx"
+ },
+ {
+ "timestamp": 1730355000,
+ "attachments": [
+ {
+ "data": [
+ {
+ "media": {
+ "uri": "photos_and_videos/your_posts/200x200png.png",
+ "creation_timestamp": 1729825000,
+ "media_metadata": {
+ "photo_metadata": {
+ "exif_data": [
+ {
+ "iso": 69,
+ "focal_length": "some/path",
+ "upload_ip": "1.1.1.1",
+ "taken_timestamp": 1717585000,
+ "modified_timestamp": 1717585000,
+ "camera_make": "xxx",
+ "camera_model": "xxx",
+ "exposure": "some/path",
+ "f_stop": "some/path",
+ "orientation": 69,
+ "original_width": 69,
+ "original_height": 69
+ }
+ ]
+ }
+ },
+ "title": "xxx",
+ "description": "xxx"
+ }
+ }
+ ]
+ }
+ ],
+ "data": [
+ {
+ "post": "xxx"
+ }
+ ],
+ "title": "xxx"
+ }
+]
diff --git a/test/fixtures/facebook-json-2021-05-01/privacy_checkup/no-data.txt b/test/fixtures/facebook-json-2021-05-01/privacy_checkup/no-data.txt
new file mode 100644
index 0000000..25340eb
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/privacy_checkup/no-data.txt
@@ -0,0 +1 @@
+You have no data in this section
\ No newline at end of file
diff --git a/test/fixtures/facebook-json-2021-05-01/profile_information/profile_information.json b/test/fixtures/facebook-json-2021-05-01/profile_information/profile_information.json
new file mode 100644
index 0000000..b65afd7
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/profile_information/profile_information.json
@@ -0,0 +1,153 @@
+{
+ "profile": {
+ "name": {
+ "full_name": "xxx",
+ "first_name": "xxx",
+ "middle_name": "xxx",
+ "last_name": "xxx"
+ },
+ "emails": {
+ "emails": [
+ "not_a_real_email@example.com",
+ "not_a_real_email@example.com"
+ ],
+ "previous_emails": [
+ "not_a_real_email@example.com"
+ ],
+ "pending_emails": [],
+ "ad_account_emails": []
+ },
+ "birthday": {
+ "year": 69,
+ "month": 69,
+ "day": 69
+ },
+ "gender": {
+ "custom_genders": [
+ "xxx"
+ ],
+ "pronoun": "xxx"
+ },
+ "previous_names": [],
+ "other_names": [],
+ "current_city": {
+ "name": "xxx",
+ "timestamp": 69
+ },
+ "hometown": {
+ "name": "xxx",
+ "timestamp": 69
+ },
+ "relationship": {
+ "status": "xxx",
+ "timestamp": 1708345000
+ },
+ "family_members": [
+ {
+ "name": "xxx",
+ "relation": "xxx",
+ "timestamp": 1727220000
+ },
+ {
+ "name": "xxx",
+ "relation": "xxx",
+ "timestamp": 1715325000
+ }
+ ],
+ "education_experiences": [
+ {
+ "name": "xxx",
+ "start_timestamp": 1735475000,
+ "end_timestamp": 1735475000,
+ "graduated": false,
+ "concentrations": [],
+ "school_type": "xxx",
+ "timestamp": 1704850000
+ },
+ {
+ "name": "xxx",
+ "graduated": true,
+ "concentrations": [],
+ "school_type": "xxx",
+ "timestamp": 1709385000
+ }
+ ],
+ "work_experiences": [
+ {
+ "employer": "xxx",
+ "title": "xxx",
+ "description": "xxx",
+ "start_timestamp": 1735510000,
+ "end_timestamp": 1728205000,
+ "timestamp": 1728205000
+ },
+ {
+ "employer": "xxx",
+ "title": "xxx",
+ "description": "xxx",
+ "start_timestamp": 1735465000,
+ "end_timestamp": 1735445000,
+ "timestamp": 1728205000
+ }
+ ],
+ "languages": [
+ {
+ "name": "xxx",
+ "timestamp": 69
+ }
+ ],
+ "interested_in": [
+ "xxx",
+ "xxx"
+ ],
+ "blood_info": {
+ "blood_donor_status": "xxx"
+ },
+ "websites": [
+ {
+ "address": "url://somewhere"
+ }
+ ],
+ "phone_numbers": [],
+ "pages": [
+ {
+ "name": "xxx",
+ "pages": [
+ "xxx",
+ "xxx"
+ ],
+ "timestamps": [
+ 69,
+ 69
+ ]
+ },
+ {
+ "name": "xxx",
+ "pages": [
+ "xxx",
+ "xxx"
+ ],
+ "timestamps": [
+ 69,
+ 69
+ ]
+ }
+ ],
+ "groups": [
+ {
+ "name": "xxx",
+ "timestamp": 1705215000
+ },
+ {
+ "name": "xxx",
+ "timestamp": 1704480000
+ }
+ ],
+ "registration_timestamp": 1718825000,
+ "profile_uri": "url://somewhere",
+ "intro_bio": {
+ "name": "xxx",
+ "timestamp": 1717060000
+ }
+ }
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/profile_information/profile_update_history.json b/test/fixtures/facebook-json-2021-05-01/profile_information/profile_update_history.json
new file mode 100644
index 0000000..6f81287
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/profile_information/profile_update_history.json
@@ -0,0 +1,73 @@
+{
+ "profile_updates": [
+ {
+ "timestamp": 1728205000,
+ "attachments": [
+ {
+ "data": [
+ {
+ "life_event": {
+ "title": "xxx",
+ "start_date": {
+ "year": 69,
+ "month": 69,
+ "day": 69
+ },
+ "end_date": {
+ "year": 69,
+ "month": 69,
+ "day": 69
+ },
+ "place": {
+ "name": "xxx",
+ "coordinate": {
+ "latitude": 69,
+ "longitude": 69
+ },
+ "address": "xxx"
+ }
+ }
+ }
+ ]
+ }
+ ],
+ "data": [
+ {
+ "backdated_timestamp": 1728025000
+ }
+ ]
+ },
+ {
+ "timestamp": 1728205000,
+ "attachments": [
+ {
+ "data": [
+ {
+ "life_event": {
+ "title": "xxx",
+ "start_date": {
+ "year": 69,
+ "month": 69,
+ "day": 69
+ },
+ "place": {
+ "name": "xxx",
+ "coordinate": {
+ "latitude": 69,
+ "longitude": 69
+ },
+ "address": "xxx"
+ }
+ }
+ }
+ ]
+ }
+ ],
+ "data": [
+ {
+ "backdated_timestamp": 1735335000
+ }
+ ]
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/rewards/no-data.txt b/test/fixtures/facebook-json-2021-05-01/rewards/no-data.txt
new file mode 100644
index 0000000..25340eb
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/rewards/no-data.txt
@@ -0,0 +1 @@
+You have no data in this section
\ No newline at end of file
diff --git a/test/fixtures/facebook-json-2021-05-01/saved_items_and_collections/no-data.txt b/test/fixtures/facebook-json-2021-05-01/saved_items_and_collections/no-data.txt
new file mode 100644
index 0000000..25340eb
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/saved_items_and_collections/no-data.txt
@@ -0,0 +1 @@
+You have no data in this section
\ No newline at end of file
diff --git a/test/fixtures/facebook-json-2021-05-01/search_history/your_search_history.json b/test/fixtures/facebook-json-2021-05-01/search_history/your_search_history.json
new file mode 100644
index 0000000..4f9e28d
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/search_history/your_search_history.json
@@ -0,0 +1,40 @@
+{
+ "searches": [
+ {
+ "timestamp": 1731825000,
+ "attachments": [
+ {
+ "data": [
+ {
+ "text": "xxx"
+ }
+ ]
+ }
+ ],
+ "data": [
+ {
+ "text": "xxx"
+ }
+ ],
+ "title": "xxx"
+ },
+ {
+ "timestamp": 1731825000,
+ "attachments": [
+ {
+ "data": [
+ {
+ "text": "xxx"
+ }
+ ]
+ }
+ ],
+ "data": [
+ {
+ "text": "xxx"
+ }
+ ],
+ "title": "xxx"
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/security_and_login_information/account_activity.json b/test/fixtures/facebook-json-2021-05-01/security_and_login_information/account_activity.json
new file mode 100644
index 0000000..c8ccab5
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/security_and_login_information/account_activity.json
@@ -0,0 +1,26 @@
+{
+ "account_activity": [
+ {
+ "action": "xxx",
+ "timestamp": 1714550000,
+ "ip_address": "1.1.1.1",
+ "user_agent": "some/path",
+ "datr_cookie": "xxx",
+ "city": "xxx",
+ "region": "xxx",
+ "country": "xxx",
+ "site_name": "xxx"
+ },
+ {
+ "action": "xxx",
+ "timestamp": 1714550000,
+ "ip_address": "1.1.1.1",
+ "user_agent": "some/path",
+ "datr_cookie": "xxx",
+ "city": "xxx",
+ "region": "xxx",
+ "country": "xxx",
+ "site_name": "xxx"
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/security_and_login_information/account_status_changes.json b/test/fixtures/facebook-json-2021-05-01/security_and_login_information/account_status_changes.json
new file mode 100644
index 0000000..2223e84
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/security_and_login_information/account_status_changes.json
@@ -0,0 +1,12 @@
+{
+ "account_status_changes": [
+ {
+ "status": "xxx",
+ "timestamp": 1714550000
+ },
+ {
+ "status": "xxx",
+ "timestamp": 1707835000
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/security_and_login_information/administrative_records.json b/test/fixtures/facebook-json-2021-05-01/security_and_login_information/administrative_records.json
new file mode 100644
index 0000000..f799a43
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/security_and_login_information/administrative_records.json
@@ -0,0 +1,22 @@
+{
+ "admin_records": [
+ {
+ "event": "xxx",
+ "session": {
+ "created_timestamp": 1714550000,
+ "ip_address": "1.1.1.1",
+ "user_agent": "some/path",
+ "datr_cookie": "xxx"
+ }
+ },
+ {
+ "event": "xxx",
+ "session": {
+ "created_timestamp": 1707835000,
+ "ip_address": "1.1.1.1",
+ "user_agent": "some/path",
+ "datr_cookie": "xxx"
+ }
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/security_and_login_information/authorized_logins.json b/test/fixtures/facebook-json-2021-05-01/security_and_login_information/authorized_logins.json
new file mode 100644
index 0000000..1a2bbee
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/security_and_login_information/authorized_logins.json
@@ -0,0 +1,12 @@
+{
+ "recognized_devices": [
+ {
+ "name": "xxx",
+ "created_timestamp": 1724290000,
+ "updated_timestamp": 1715440000,
+ "ip_address": "1.1.1.1",
+ "user_agent": "some/path",
+ "datr_cookie": "xxx"
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/security_and_login_information/contact_verifications.json b/test/fixtures/facebook-json-2021-05-01/security_and_login_information/contact_verifications.json
new file mode 100644
index 0000000..c22059a
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/security_and_login_information/contact_verifications.json
@@ -0,0 +1,14 @@
+{
+ "contact_verifications": [
+ {
+ "contact": "not_a_real_email@example.com",
+ "contact_type": 69,
+ "verification_time": 1729235000
+ },
+ {
+ "contact": "not_a_real_email@example.com",
+ "contact_type": 69,
+ "verification_time": 1705875000
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/security_and_login_information/datr_cookie_info.json b/test/fixtures/facebook-json-2021-05-01/security_and_login_information/datr_cookie_info.json
new file mode 100644
index 0000000..904f6d8
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/security_and_login_information/datr_cookie_info.json
@@ -0,0 +1,7 @@
+{
+ "datr_stats": {
+ "xxxxxx": [
+ 1704880000
+ ]
+ }
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/security_and_login_information/login_protection_data.json b/test/fixtures/facebook-json-2021-05-01/security_and_login_information/login_protection_data.json
new file mode 100644
index 0000000..cdc395b
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/security_and_login_information/login_protection_data.json
@@ -0,0 +1,19 @@
+{
+ "login_protection_data": [
+ {
+ "name": "xxx",
+ "session": {
+ "created_timestamp": 1707325000,
+ "updated_timestamp": 1714550000,
+ "ip_address": "1.1.1.1"
+ }
+ },
+ {
+ "name": "xxx",
+ "session": {
+ "created_timestamp": 1727405000,
+ "updated_timestamp": 1714550000
+ }
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/security_and_login_information/logins_and_logouts.json b/test/fixtures/facebook-json-2021-05-01/security_and_login_information/logins_and_logouts.json
new file mode 100644
index 0000000..f4a2169
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/security_and_login_information/logins_and_logouts.json
@@ -0,0 +1,16 @@
+{
+ "account_accesses": [
+ {
+ "action": "xxx",
+ "timestamp": 1714550000,
+ "site": "xxx",
+ "ip_address": "1.1.1.1"
+ },
+ {
+ "action": "xxx",
+ "timestamp": 1713895000,
+ "site": "xxx",
+ "ip_address": "1.1.1.1"
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/security_and_login_information/mobile_devices.json b/test/fixtures/facebook-json-2021-05-01/security_and_login_information/mobile_devices.json
new file mode 100644
index 0000000..60469b7
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/security_and_login_information/mobile_devices.json
@@ -0,0 +1,20 @@
+{
+ "devices": [
+ {
+ "type": "xxx",
+ "os": "xxx",
+ "update_time": 1714475000,
+ "family_device_id": "xxx",
+ "device_locale": "xxx"
+ },
+ {
+ "type": "xxx",
+ "os": "xxx",
+ "update_time": 1708200000,
+ "advertiser_id": "xxx",
+ "udid": "xxx",
+ "family_device_id": "xxx",
+ "device_locale": "xxx"
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/security_and_login_information/used_ip_addresses.json b/test/fixtures/facebook-json-2021-05-01/security_and_login_information/used_ip_addresses.json
new file mode 100644
index 0000000..97d127c
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/security_and_login_information/used_ip_addresses.json
@@ -0,0 +1,14 @@
+{
+ "used_ip_address": [
+ {
+ "ip": "1.1.1.1",
+ "action": "xxx",
+ "timestamp": 1734150000
+ },
+ {
+ "ip": "1.1.1.1",
+ "action": "xxx",
+ "timestamp": 1728420000
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/security_and_login_information/where_you're_logged_in.json b/test/fixtures/facebook-json-2021-05-01/security_and_login_information/where_you're_logged_in.json
new file mode 100644
index 0000000..2090494
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/security_and_login_information/where_you're_logged_in.json
@@ -0,0 +1,25 @@
+{
+ "active_sessions": [
+ {
+ "created_timestamp": 1714550000,
+ "ip_address": "1.1.1.1",
+ "user_agent": "some/path",
+ "datr_cookie": "xxx",
+ "device": "xxx",
+ "location": "xxx",
+ "app": "xxx",
+ "session_type": "xxx"
+ },
+ {
+ "created_timestamp": 1711775000,
+ "updated_timestamp": 1714530000,
+ "ip_address": "1.1.1.1",
+ "user_agent": "some/path",
+ "datr_cookie": "xxx",
+ "device": "xxx",
+ "location": "xxx",
+ "app": "xxx",
+ "session_type": "xxx"
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/security_and_login_information/your_facebook_activity.json b/test/fixtures/facebook-json-2021-05-01/security_and_login_information/your_facebook_activity.json
new file mode 100644
index 0000000..1f67edf
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/security_and_login_information/your_facebook_activity.json
@@ -0,0 +1,21 @@
+{
+ "last_activity": {
+ "last_activity_time": {
+ "Website": {
+ "activity_by_day": [
+ 1714420000
+ ]
+ },
+ "Facebook app": {
+ "activity_by_day": [
+ 1713815000
+ ]
+ },
+ "Messenger Lite for Android": {
+ "activity_by_day": [
+ 1713815000
+ ]
+ }
+ }
+ }
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/short_videos/no-data.txt b/test/fixtures/facebook-json-2021-05-01/short_videos/no-data.txt
new file mode 100644
index 0000000..25340eb
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/short_videos/no-data.txt
@@ -0,0 +1 @@
+You have no data in this section
\ No newline at end of file
diff --git a/test/fixtures/facebook-json-2021-05-01/stories/story_reactions.json b/test/fixtures/facebook-json-2021-05-01/stories/story_reactions.json
new file mode 100644
index 0000000..d557f7e
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/stories/story_reactions.json
@@ -0,0 +1,12 @@
+{
+ "stories_feedback": [
+ {
+ "timestamp": 1705215000,
+ "title": "xxx"
+ },
+ {
+ "timestamp": 1714335000,
+ "title": "xxx"
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/trash/no-data.txt b/test/fixtures/facebook-json-2021-05-01/trash/no-data.txt
new file mode 100644
index 0000000..25340eb
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/trash/no-data.txt
@@ -0,0 +1 @@
+You have no data in this section
\ No newline at end of file
diff --git a/test/fixtures/facebook-json-2021-05-01/voice_recording_and_transcription/no-data.txt b/test/fixtures/facebook-json-2021-05-01/voice_recording_and_transcription/no-data.txt
new file mode 100644
index 0000000..25340eb
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/voice_recording_and_transcription/no-data.txt
@@ -0,0 +1 @@
+You have no data in this section
\ No newline at end of file
diff --git a/test/fixtures/facebook-json-2021-05-01/volunteering/no-data.txt b/test/fixtures/facebook-json-2021-05-01/volunteering/no-data.txt
new file mode 100644
index 0000000..25340eb
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/volunteering/no-data.txt
@@ -0,0 +1 @@
+You have no data in this section
\ No newline at end of file
diff --git a/test/fixtures/facebook-json-2021-05-01/voting_location_and_reminders/location.json b/test/fixtures/facebook-json-2021-05-01/voting_location_and_reminders/location.json
new file mode 100644
index 0000000..baf1655
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/voting_location_and_reminders/location.json
@@ -0,0 +1,5 @@
+{
+ "voting_location": {
+ "voting_location": "xxx"
+ }
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/voting_location_and_reminders/voting_reminders.json b/test/fixtures/facebook-json-2021-05-01/voting_location_and_reminders/voting_reminders.json
new file mode 100644
index 0000000..855264f
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/voting_location_and_reminders/voting_reminders.json
@@ -0,0 +1,5 @@
+{
+ "voting_reminders": {
+ "voting_reminders": "xxx"
+ }
+}
diff --git a/test/fixtures/facebook-json-2021-05-01/your_places/no-data.txt b/test/fixtures/facebook-json-2021-05-01/your_places/no-data.txt
new file mode 100644
index 0000000..25340eb
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/your_places/no-data.txt
@@ -0,0 +1 @@
+You have no data in this section
\ No newline at end of file
diff --git a/test/fixtures/facebook-json-2021-05-01/your_topics/no-data.txt b/test/fixtures/facebook-json-2021-05-01/your_topics/no-data.txt
new file mode 100644
index 0000000..25340eb
--- /dev/null
+++ b/test/fixtures/facebook-json-2021-05-01/your_topics/no-data.txt
@@ -0,0 +1 @@
+You have no data in this section
\ No newline at end of file
diff --git a/test/fixtures/facebook-json-2025-11-29/ads_information/ad_preferences.json b/test/fixtures/facebook-json-2025-11-29/ads_information/ad_preferences.json
new file mode 100644
index 0000000..c01cdaf
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/ads_information/ad_preferences.json
@@ -0,0 +1,14 @@
+{
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/ads_information/ads_about_meta.json b/test/fixtures/facebook-json-2025-11-29/ads_information/ads_about_meta.json
new file mode 100644
index 0000000..df9a24e
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/ads_information/ads_about_meta.json
@@ -0,0 +1,14 @@
+{
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "timestamp_value": 69
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/ads_information/advertisers_you've_interacted_with.json b/test/fixtures/facebook-json-2025-11-29/ads_information/advertisers_you've_interacted_with.json
new file mode 100644
index 0000000..170f30a
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/ads_information/advertisers_you've_interacted_with.json
@@ -0,0 +1,30 @@
+[
+ {
+ "timestamp": 1727295000,
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "url://somewhere"
+ }
+ ],
+ "fbid": "xxx"
+ },
+ {
+ "timestamp": 1727295000,
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+ }
+]
diff --git a/test/fixtures/facebook-json-2025-11-29/ads_information/meta_ad_library_accounts.json b/test/fixtures/facebook-json-2025-11-29/ads_information/meta_ad_library_accounts.json
new file mode 100644
index 0000000..4b5a855
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/ads_information/meta_ad_library_accounts.json
@@ -0,0 +1,15 @@
+{
+ "timestamp": 1729390000,
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/ads_information/story_views_in_past_7_days.json b/test/fixtures/facebook-json-2025-11-29/ads_information/story_views_in_past_7_days.json
new file mode 100644
index 0000000..2fd9c34
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/ads_information/story_views_in_past_7_days.json
@@ -0,0 +1,10 @@
+{
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/ads_information/your_meta_business_suite_guidance_interactions.json b/test/fixtures/facebook-json-2025-11-29/ads_information/your_meta_business_suite_guidance_interactions.json
new file mode 100644
index 0000000..becab50
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/ads_information/your_meta_business_suite_guidance_interactions.json
@@ -0,0 +1,30 @@
+[
+ {
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "timestamp_value": 1723195000
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+ },
+ {
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "timestamp_value": 1723035000
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+ }
+]
diff --git a/test/fixtures/facebook-json-2025-11-29/ads_information/your_recent_reported_conversions.json b/test/fixtures/facebook-json-2025-11-29/ads_information/your_recent_reported_conversions.json
new file mode 100644
index 0000000..91cc007
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/ads_information/your_recent_reported_conversions.json
@@ -0,0 +1,22 @@
+[
+ {
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "timestamp_value": 1731795000
+ }
+ ],
+ "fbid": "xxx"
+ },
+ {
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "timestamp_value": 1730410000
+ }
+ ],
+ "fbid": "xxx"
+ }
+]
diff --git a/test/fixtures/facebook-json-2025-11-29/ads_information/your_sampled_locations.json b/test/fixtures/facebook-json-2025-11-29/ads_information/your_sampled_locations.json
new file mode 100644
index 0000000..753e786
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/ads_information/your_sampled_locations.json
@@ -0,0 +1,15 @@
+{
+ "timestamp": 1711190000,
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/apps_and_websites_off_of_facebook/connected_apps_and_websites.json b/test/fixtures/facebook-json-2025-11-29/apps_and_websites_off_of_facebook/connected_apps_and_websites.json
new file mode 100644
index 0000000..1629f1f
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/apps_and_websites_off_of_facebook/connected_apps_and_websites.json
@@ -0,0 +1,18 @@
+{
+ "installed_apps_v2": [
+ {
+ "name": "xxx",
+ "added_timestamp": 1705020000,
+ "user_app_scoped_id": 69,
+ "category": "xxx",
+ "removed_timestamp": 69
+ },
+ {
+ "name": "xxx",
+ "added_timestamp": 1718990000,
+ "user_app_scoped_id": 69,
+ "category": "xxx",
+ "removed_timestamp": 69
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/apps_and_websites_off_of_facebook/permissions_you_have_granted_to_apps.json b/test/fixtures/facebook-json-2025-11-29/apps_and_websites_off_of_facebook/permissions_you_have_granted_to_apps.json
new file mode 100644
index 0000000..97419a9
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/apps_and_websites_off_of_facebook/permissions_you_have_granted_to_apps.json
@@ -0,0 +1,18 @@
+{
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "vec": [
+ {
+ "value": "xxx"
+ }
+ ]
+ },
+ {
+ "label": "xxx",
+ "timestamp_value": 1707335000
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/apps_and_websites_off_of_facebook/your_activity_off_meta_technologies_settings.json b/test/fixtures/facebook-json-2025-11-29/apps_and_websites_off_of_facebook/your_activity_off_meta_technologies_settings.json
new file mode 100644
index 0000000..233cf23
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/apps_and_websites_off_of_facebook/your_activity_off_meta_technologies_settings.json
@@ -0,0 +1,15 @@
+{
+ "timestamp": 1724455000,
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "dict": []
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/connections/friends/people_you_may_know.json b/test/fixtures/facebook-json-2025-11-29/connections/friends/people_you_may_know.json
new file mode 100644
index 0000000..2a65c42
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/connections/friends/people_you_may_know.json
@@ -0,0 +1,17 @@
+{
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "vec": [
+ {
+ "value": "xxx"
+ },
+ {
+ "value": "xxx"
+ }
+ ]
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/connections/friends/received_friend_requests.json b/test/fixtures/facebook-json-2025-11-29/connections/friends/received_friend_requests.json
new file mode 100644
index 0000000..0cce056
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/connections/friends/received_friend_requests.json
@@ -0,0 +1,12 @@
+{
+ "received_requests_v2": [
+ {
+ "name": "xxx",
+ "timestamp": 1725965000
+ },
+ {
+ "name": "xxx",
+ "timestamp": 1725280000
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/connections/friends/rejected_friend_requests.json b/test/fixtures/facebook-json-2025-11-29/connections/friends/rejected_friend_requests.json
new file mode 100644
index 0000000..5c71633
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/connections/friends/rejected_friend_requests.json
@@ -0,0 +1,12 @@
+{
+ "rejected_requests_v2": [
+ {
+ "name": "xxx",
+ "timestamp": 1725200000
+ },
+ {
+ "name": "xxx",
+ "timestamp": 1723450000
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/connections/friends/suggested_friends.json b/test/fixtures/facebook-json-2025-11-29/connections/friends/suggested_friends.json
new file mode 100644
index 0000000..04883bf
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/connections/friends/suggested_friends.json
@@ -0,0 +1,30 @@
+[
+ {
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "timestamp_value": 1717860000
+ },
+ {
+ "label": "xxx",
+ "timestamp_value": 1717860000
+ }
+ ],
+ "fbid": "xxx"
+ },
+ {
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "timestamp_value": 1712765000
+ },
+ {
+ "label": "xxx",
+ "timestamp_value": 1712765000
+ }
+ ],
+ "fbid": "xxx"
+ }
+]
diff --git a/test/fixtures/facebook-json-2025-11-29/connections/friends/your_friends.json b/test/fixtures/facebook-json-2025-11-29/connections/friends/your_friends.json
new file mode 100644
index 0000000..89650d2
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/connections/friends/your_friends.json
@@ -0,0 +1,12 @@
+{
+ "friends_v2": [
+ {
+ "name": "xxx",
+ "timestamp": 1711990000
+ },
+ {
+ "name": "xxx",
+ "timestamp": 1725725000
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/connections/friends/your_post_audiences.json b/test/fixtures/facebook-json-2025-11-29/connections/friends/your_post_audiences.json
new file mode 100644
index 0000000..02bf686
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/connections/friends/your_post_audiences.json
@@ -0,0 +1,30 @@
+[
+ {
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+ },
+ {
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+ }
+]
diff --git a/test/fixtures/facebook-json-2025-11-29/logged_information/activity_messages/milestone_notifications.json b/test/fixtures/facebook-json-2025-11-29/logged_information/activity_messages/milestone_notifications.json
new file mode 100644
index 0000000..bd56b27
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/logged_information/activity_messages/milestone_notifications.json
@@ -0,0 +1,15 @@
+{
+ "timestamp": 1717740000,
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/logged_information/activity_messages/people_and_friends.json b/test/fixtures/facebook-json-2025-11-29/logged_information/activity_messages/people_and_friends.json
new file mode 100644
index 0000000..ca62528
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/logged_information/activity_messages/people_and_friends.json
@@ -0,0 +1,24 @@
+{
+ "people_interactions_v2": [
+ {
+ "name": "xxx",
+ "description": "xxx",
+ "entries": [
+ {
+ "timestamp": 1726085000,
+ "data": {
+ "name": "xxx",
+ "uri": "url://somewhere"
+ }
+ },
+ {
+ "timestamp": 1705755000,
+ "data": {
+ "name": "xxx",
+ "uri": "url://somewhere"
+ }
+ }
+ ]
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/logged_information/form_submissions/additional_profile_terms_and_conditions.json b/test/fixtures/facebook-json-2025-11-29/logged_information/form_submissions/additional_profile_terms_and_conditions.json
new file mode 100644
index 0000000..dcf5225
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/logged_information/form_submissions/additional_profile_terms_and_conditions.json
@@ -0,0 +1,15 @@
+{
+ "timestamp": 1723420000,
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "timestamp_value": 1723290000
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/logged_information/in-app_messages/your_in-app_messages_interactions.json b/test/fixtures/facebook-json-2025-11-29/logged_information/in-app_messages/your_in-app_messages_interactions.json
new file mode 100644
index 0000000..85ac9a1
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/logged_information/in-app_messages/your_in-app_messages_interactions.json
@@ -0,0 +1,32 @@
+[
+ {
+ "timestamp": 1730885000,
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+ },
+ {
+ "timestamp": 1714580000,
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+ }
+]
diff --git a/test/fixtures/facebook-json-2025-11-29/logged_information/interactions/groups_you've_visited.json b/test/fixtures/facebook-json-2025-11-29/logged_information/interactions/groups_you've_visited.json
new file mode 100644
index 0000000..31f257f
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/logged_information/interactions/groups_you've_visited.json
@@ -0,0 +1,24 @@
+[
+ {
+ "timestamp": 1726155000,
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+ },
+ {
+ "timestamp": 1705020000,
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+ }
+]
diff --git a/test/fixtures/facebook-json-2025-11-29/logged_information/interactions/products_that_interest_you.json b/test/fixtures/facebook-json-2025-11-29/logged_information/interactions/products_that_interest_you.json
new file mode 100644
index 0000000..4156fa2
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/logged_information/interactions/products_that_interest_you.json
@@ -0,0 +1,32 @@
+[
+ {
+ "timestamp": 1732200000,
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+ },
+ {
+ "timestamp": 1730390000,
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "some/path"
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+ }
+]
diff --git a/test/fixtures/facebook-json-2025-11-29/logged_information/interactions/recently_viewed.json b/test/fixtures/facebook-json-2025-11-29/logged_information/interactions/recently_viewed.json
new file mode 100644
index 0000000..1f83c9e
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/logged_information/interactions/recently_viewed.json
@@ -0,0 +1,65 @@
+{
+ "recently_viewed": [
+ {
+ "name": "xxx",
+ "description": "xxx",
+ "children": [
+ {
+ "name": "xxx",
+ "description": "xxx",
+ "entries": [
+ {
+ "timestamp": 1706780000,
+ "data": {
+ "uri": "url://somewhere",
+ "name": "xxx"
+ }
+ }
+ ]
+ },
+ {
+ "name": "xxx",
+ "description": "xxx",
+ "entries": [
+ {
+ "timestamp": 1735135000,
+ "data": {
+ "uri": "url://somewhere",
+ "name": "xxx",
+ "watch_position_seconds": "xxx"
+ }
+ },
+ {
+ "timestamp": 1722885000,
+ "data": {
+ "uri": "url://somewhere",
+ "name": "xxx",
+ "watch_position_seconds": "xxx"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "xxx",
+ "description": "xxx",
+ "entries": [
+ {
+ "timestamp": 1731805000,
+ "data": {
+ "name": "xxx",
+ "uri": "url://somewhere"
+ }
+ },
+ {
+ "timestamp": 1730725000,
+ "data": {
+ "name": "xxx",
+ "uri": "url://somewhere"
+ }
+ }
+ ]
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/logged_information/interactions/recently_visited.json b/test/fixtures/facebook-json-2025-11-29/logged_information/interactions/recently_visited.json
new file mode 100644
index 0000000..83ae1ef
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/logged_information/interactions/recently_visited.json
@@ -0,0 +1,37 @@
+{
+ "visited_things_v2": [
+ {
+ "name": "xxx",
+ "description": "xxx",
+ "entries": [
+ {
+ "timestamp": 1730725000,
+ "data": {
+ "name": "xxx",
+ "uri": "url://somewhere"
+ }
+ },
+ {
+ "timestamp": 1730135000,
+ "data": {
+ "name": "xxx",
+ "uri": "url://somewhere"
+ }
+ }
+ ]
+ },
+ {
+ "name": "xxx",
+ "description": "xxx",
+ "entries": [
+ {
+ "timestamp": 1730135000,
+ "data": {
+ "name": "xxx",
+ "uri": "url://somewhere"
+ }
+ }
+ ]
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/logged_information/location/primary_location.json b/test/fixtures/facebook-json-2025-11-29/logged_information/location/primary_location.json
new file mode 100644
index 0000000..1b851c1
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/logged_information/location/primary_location.json
@@ -0,0 +1,13 @@
+{
+ "primary_location_v2": {
+ "city_region_pairs": [
+ [
+ "xxx",
+ "xxx"
+ ]
+ ],
+ "zipcode": [
+ "xxx"
+ ]
+ }
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/logged_information/location/timezone.json b/test/fixtures/facebook-json-2025-11-29/logged_information/location/timezone.json
new file mode 100644
index 0000000..cc7dcd3
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/logged_information/location/timezone.json
@@ -0,0 +1,10 @@
+{
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "some/path"
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/logged_information/location/your_privacy_jurisdiction.json b/test/fixtures/facebook-json-2025-11-29/logged_information/location/your_privacy_jurisdiction.json
new file mode 100644
index 0000000..9a25dc4
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/logged_information/location/your_privacy_jurisdiction.json
@@ -0,0 +1,18 @@
+{
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "vec": [
+ {
+ "value": "xxx"
+ }
+ ]
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/logged_information/notifications/notifications.json b/test/fixtures/facebook-json-2025-11-29/logged_information/notifications/notifications.json
new file mode 100644
index 0000000..5016159
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/logged_information/notifications/notifications.json
@@ -0,0 +1,16 @@
+{
+ "notifications_v2": [
+ {
+ "timestamp": 1732105000,
+ "unread": true,
+ "href": "url://somewhere",
+ "text": "xxx"
+ },
+ {
+ "timestamp": 1731630000,
+ "unread": true,
+ "href": "url://somewhere",
+ "text": "xxx"
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/logged_information/notifications/your_notifications_tab_activity.json b/test/fixtures/facebook-json-2025-11-29/logged_information/notifications/your_notifications_tab_activity.json
new file mode 100644
index 0000000..fc2d787
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/logged_information/notifications/your_notifications_tab_activity.json
@@ -0,0 +1,14 @@
+{
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "timestamp_value": 1730675000
+ },
+ {
+ "label": "xxx",
+ "timestamp_value": 1729105000
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/logged_information/other_logged_information/consents.json b/test/fixtures/facebook-json-2025-11-29/logged_information/other_logged_information/consents.json
new file mode 100644
index 0000000..0610afe
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/logged_information/other_logged_information/consents.json
@@ -0,0 +1,15 @@
+{
+ "timestamp": 1707465000,
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "timestamp_value": 1726875000
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/logged_information/other_logged_information/detected_hardware.json b/test/fixtures/facebook-json-2025-11-29/logged_information/other_logged_information/detected_hardware.json
new file mode 100644
index 0000000..d339c9d
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/logged_information/other_logged_information/detected_hardware.json
@@ -0,0 +1,30 @@
+[
+ {
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "timestamp_value": 1730210000
+ },
+ {
+ "label": "xxx",
+ "timestamp_value": 1730210000
+ }
+ ],
+ "fbid": "xxx"
+ },
+ {
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "timestamp_value": 1723040000
+ },
+ {
+ "label": "xxx",
+ "timestamp_value": 1723825000
+ }
+ ],
+ "fbid": "xxx"
+ }
+]
diff --git a/test/fixtures/facebook-json-2025-11-29/logged_information/other_logged_information/facebook_reels_usage_information.json b/test/fixtures/facebook-json-2025-11-29/logged_information/other_logged_information/facebook_reels_usage_information.json
new file mode 100644
index 0000000..aacf897
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/logged_information/other_logged_information/facebook_reels_usage_information.json
@@ -0,0 +1,19 @@
+{
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "dict": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ]
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/logged_information/other_logged_information/locations_of_interest.json b/test/fixtures/facebook-json-2025-11-29/logged_information/other_logged_information/locations_of_interest.json
new file mode 100644
index 0000000..f487235
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/logged_information/other_logged_information/locations_of_interest.json
@@ -0,0 +1,21 @@
+{
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "vec": [
+ {
+ "value": "xxx"
+ },
+ {
+ "value": "xxx"
+ }
+ ]
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/logged_information/other_logged_information/your_facebook_watch_activity_in_the_last_28_days.json b/test/fixtures/facebook-json-2025-11-29/logged_information/other_logged_information/your_facebook_watch_activity_in_the_last_28_days.json
new file mode 100644
index 0000000..2fd9c34
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/logged_information/other_logged_information/your_facebook_watch_activity_in_the_last_28_days.json
@@ -0,0 +1,10 @@
+{
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/logged_information/other_logged_information/your_privacy_checkup_interactions.json b/test/fixtures/facebook-json-2025-11-29/logged_information/other_logged_information/your_privacy_checkup_interactions.json
new file mode 100644
index 0000000..a650378
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/logged_information/other_logged_information/your_privacy_checkup_interactions.json
@@ -0,0 +1,14 @@
+{
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "timestamp_value": 1707750000
+ },
+ {
+ "label": "xxx",
+ "timestamp_value": 69
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/logged_information/search/your_search_history.json b/test/fixtures/facebook-json-2025-11-29/logged_information/search/your_search_history.json
new file mode 100644
index 0000000..4d57877
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/logged_information/search/your_search_history.json
@@ -0,0 +1,40 @@
+{
+ "searches_v2": [
+ {
+ "timestamp": 1733650000,
+ "attachments": [
+ {
+ "data": [
+ {
+ "text": "xxx"
+ }
+ ]
+ }
+ ],
+ "data": [
+ {
+ "text": "xxx"
+ }
+ ],
+ "title": "xxx"
+ },
+ {
+ "timestamp": 1735345000,
+ "attachments": [
+ {
+ "data": [
+ {
+ "text": "xxx"
+ }
+ ]
+ }
+ ],
+ "data": [
+ {
+ "text": "xxx"
+ }
+ ],
+ "title": "xxx"
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/personal_information/facebook_accounts_center/your_account_password_information.json b/test/fixtures/facebook-json-2025-11-29/personal_information/facebook_accounts_center/your_account_password_information.json
new file mode 100644
index 0000000..44fa1e9
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/personal_information/facebook_accounts_center/your_account_password_information.json
@@ -0,0 +1,10 @@
+{
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "timestamp_value": 1707335000
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/personal_information/other_personal_information/emails_we_sent_you.json b/test/fixtures/facebook-json-2025-11-29/personal_information/other_personal_information/emails_we_sent_you.json
new file mode 100644
index 0000000..e8077af
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/personal_information/other_personal_information/emails_we_sent_you.json
@@ -0,0 +1,15 @@
+{
+ "title": "xxx",
+ "timestamp": 1732465000,
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/personal_information/other_personal_information/notifications_about_new_users_joining_facebook.json b/test/fixtures/facebook-json-2025-11-29/personal_information/other_personal_information/notifications_about_new_users_joining_facebook.json
new file mode 100644
index 0000000..c7178c1
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/personal_information/other_personal_information/notifications_about_new_users_joining_facebook.json
@@ -0,0 +1,22 @@
+[
+ {
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+ },
+ {
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+ }
+]
diff --git a/test/fixtures/facebook-json-2025-11-29/personal_information/other_personal_information/your_media_permissions.json b/test/fixtures/facebook-json-2025-11-29/personal_information/other_personal_information/your_media_permissions.json
new file mode 100644
index 0000000..c01cdaf
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/personal_information/other_personal_information/your_media_permissions.json
@@ -0,0 +1,14 @@
+{
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/personal_information/profile_information/contact_info.json b/test/fixtures/facebook-json-2025-11-29/personal_information/profile_information/contact_info.json
new file mode 100644
index 0000000..93824de
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/personal_information/profile_information/contact_info.json
@@ -0,0 +1,14 @@
+{
+ "timestamp": 1707465000,
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/personal_information/profile_information/contacts_sync_settings.json b/test/fixtures/facebook-json-2025-11-29/personal_information/profile_information/contacts_sync_settings.json
new file mode 100644
index 0000000..02bf686
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/personal_information/profile_information/contacts_sync_settings.json
@@ -0,0 +1,30 @@
+[
+ {
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+ },
+ {
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+ }
+]
diff --git a/test/fixtures/facebook-json-2025-11-29/personal_information/profile_information/predicted_languages.json b/test/fixtures/facebook-json-2025-11-29/personal_information/profile_information/predicted_languages.json
new file mode 100644
index 0000000..ecbc82a
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/personal_information/profile_information/predicted_languages.json
@@ -0,0 +1,15 @@
+{
+ "timestamp": 1723315000,
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/personal_information/profile_information/previous_contact_information.json b/test/fixtures/facebook-json-2025-11-29/personal_information/profile_information/previous_contact_information.json
new file mode 100644
index 0000000..2fd9c34
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/personal_information/profile_information/previous_contact_information.json
@@ -0,0 +1,10 @@
+{
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/personal_information/profile_information/profile_information.json b/test/fixtures/facebook-json-2025-11-29/personal_information/profile_information/profile_information.json
new file mode 100644
index 0000000..519e52b
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/personal_information/profile_information/profile_information.json
@@ -0,0 +1,41 @@
+{
+ "profile_v2": {
+ "name": {
+ "full_name": "xxx",
+ "first_name": "xxx",
+ "middle_name": "xxx",
+ "last_name": "xxx"
+ },
+ "emails": {
+ "emails": [
+ "not_a_real_email@example.com"
+ ],
+ "previous_emails": [],
+ "pending_emails": [],
+ "ad_account_emails": []
+ },
+ "birthday": {
+ "year": 69,
+ "month": 69,
+ "day": 69
+ },
+ "gender": {
+ "custom_genders": [
+ "xxx"
+ ],
+ "pronoun": "xxx"
+ },
+ "previous_names": [],
+ "other_names": [],
+ "education_experiences": [],
+ "work_experiences": [],
+ "websites": [],
+ "phone_numbers": [],
+ "registration_timestamp": 1707465000,
+ "profile_uri": "url://somewhere",
+ "intro_bio": {
+ "name": "some/path",
+ "timestamp": 1707875000
+ }
+ }
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/personal_information/profile_information/profile_update_history.json b/test/fixtures/facebook-json-2025-11-29/personal_information/profile_information/profile_update_history.json
new file mode 100644
index 0000000..cb531fc
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/personal_information/profile_information/profile_update_history.json
@@ -0,0 +1,43 @@
+{
+ "profile_updates_v2": [
+ {
+ "timestamp": 1707335000,
+ "attachments": [
+ {
+ "data": [
+ {
+ "life_event": {
+ "title": "xxx",
+ "start_date": {
+ "year": 69,
+ "month": 69,
+ "day": 69
+ }
+ }
+ }
+ ]
+ }
+ ],
+ "data": [
+ {
+ "backdated_timestamp": 69
+ },
+ {}
+ ],
+ "title": "xxx"
+ },
+ {
+ "timestamp": 1707340000,
+ "attachments": [
+ {
+ "data": [
+ {
+ "text": "xxx"
+ }
+ ]
+ }
+ ],
+ "title": "xxx"
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/personal_information/profile_information/the_ways_we_can_send_you_notifications.json b/test/fixtures/facebook-json-2025-11-29/personal_information/profile_information/the_ways_we_can_send_you_notifications.json
new file mode 100644
index 0000000..c01cdaf
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/personal_information/profile_information/the_ways_we_can_send_you_notifications.json
@@ -0,0 +1,14 @@
+{
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/personal_information/profile_information/your_devices.json b/test/fixtures/facebook-json-2025-11-29/personal_information/profile_information/your_devices.json
new file mode 100644
index 0000000..02bf686
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/personal_information/profile_information/your_devices.json
@@ -0,0 +1,30 @@
+[
+ {
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+ },
+ {
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+ }
+]
diff --git a/test/fixtures/facebook-json-2025-11-29/preferences/feed/controls.json b/test/fixtures/facebook-json-2025-11-29/preferences/feed/controls.json
new file mode 100644
index 0000000..2a144d9
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/preferences/feed/controls.json
@@ -0,0 +1,14 @@
+{
+ "controls": [
+ {
+ "name": "xxx",
+ "description": "xxx",
+ "entries": []
+ },
+ {
+ "name": "xxx",
+ "description": "xxx",
+ "entries": []
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/preferences/feed/feed.json b/test/fixtures/facebook-json-2025-11-29/preferences/feed/feed.json
new file mode 100644
index 0000000..64fae4d
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/preferences/feed/feed.json
@@ -0,0 +1,24 @@
+{
+ "people_and_friends_v2": [
+ {
+ "name": "xxx",
+ "description": "xxx",
+ "entries": [
+ {
+ "timestamp": 1708220000,
+ "data": {
+ "name": "xxx",
+ "uri": "url://somewhere"
+ }
+ },
+ {
+ "timestamp": 1723610000,
+ "data": {
+ "name": "xxx",
+ "uri": "url://somewhere"
+ }
+ }
+ ]
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/preferences/feed/reduce.json b/test/fixtures/facebook-json-2025-11-29/preferences/feed/reduce.json
new file mode 100644
index 0000000..4b5a855
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/preferences/feed/reduce.json
@@ -0,0 +1,15 @@
+{
+ "timestamp": 1729390000,
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/preferences/feed/unfollowed_profiles.json b/test/fixtures/facebook-json-2025-11-29/preferences/feed/unfollowed_profiles.json
new file mode 100644
index 0000000..2e0d6ae
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/preferences/feed/unfollowed_profiles.json
@@ -0,0 +1,14 @@
+{
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "timestamp_value": 1708485000
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/preferences/memories/settings.json b/test/fixtures/facebook-json-2025-11-29/preferences/memories/settings.json
new file mode 100644
index 0000000..8e6ee26
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/preferences/memories/settings.json
@@ -0,0 +1,14 @@
+{
+ "timestamp": 1713235000,
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx"
+ },
+ {
+ "label": "xxx",
+ "vec": []
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/preferences/preferences/language_and_locale.json b/test/fixtures/facebook-json-2025-11-29/preferences/preferences/language_and_locale.json
new file mode 100644
index 0000000..adb5c73
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/preferences/preferences/language_and_locale.json
@@ -0,0 +1,44 @@
+{
+ "language_and_locale_v2": [
+ {
+ "name": "xxx",
+ "description": "xxx",
+ "children": [
+ {
+ "name": "xxx",
+ "description": "xxx",
+ "entries": [
+ {
+ "data": {
+ "value": "xxx"
+ }
+ }
+ ]
+ },
+ {
+ "name": "xxx",
+ "description": "xxx",
+ "entries": [
+ {
+ "timestamp": 1706545000,
+ "data": {
+ "name": "xxx"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "xxx",
+ "description": "xxx",
+ "entries": [
+ {
+ "data": {
+ "value": "xxx"
+ }
+ }
+ ]
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/preferences/preferences/language_settings_history.json b/test/fixtures/facebook-json-2025-11-29/preferences/preferences/language_settings_history.json
new file mode 100644
index 0000000..de90398
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/preferences/preferences/language_settings_history.json
@@ -0,0 +1,24 @@
+{
+ "timestamp": 1707465000,
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "vec": [
+ {
+ "dict": [
+ {
+ "label": "xxx",
+ "timestamp_value": 1706545000
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/preferences/preferences/login_alerts_settings.json b/test/fixtures/facebook-json-2025-11-29/preferences/preferences/login_alerts_settings.json
new file mode 100644
index 0000000..c01cdaf
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/preferences/preferences/login_alerts_settings.json
@@ -0,0 +1,14 @@
+{
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/preferences/preferences/notification_settings.json b/test/fixtures/facebook-json-2025-11-29/preferences/preferences/notification_settings.json
new file mode 100644
index 0000000..78c00ed
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/preferences/preferences/notification_settings.json
@@ -0,0 +1,39 @@
+{
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "vec": [
+ {
+ "dict": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ]
+ },
+ {
+ "dict": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/preferences/preferences/privacy_settings.json b/test/fixtures/facebook-json-2025-11-29/preferences/preferences/privacy_settings.json
new file mode 100644
index 0000000..aacf897
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/preferences/preferences/privacy_settings.json
@@ -0,0 +1,19 @@
+{
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "dict": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ]
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/preferences/preferences/reels_preferences.json b/test/fixtures/facebook-json-2025-11-29/preferences/preferences/reels_preferences.json
new file mode 100644
index 0000000..c01cdaf
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/preferences/preferences/reels_preferences.json
@@ -0,0 +1,14 @@
+{
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/preferences/preferences/video.json b/test/fixtures/facebook-json-2025-11-29/preferences/preferences/video.json
new file mode 100644
index 0000000..9534dbc
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/preferences/preferences/video.json
@@ -0,0 +1,16 @@
+{
+ "watch_videos_v2": [
+ {
+ "video_title": "xxx",
+ "user_action": "xxx",
+ "action_time": "xxx",
+ "feedback_collection": "xxx"
+ },
+ {
+ "video_title": "xxx",
+ "user_action": "xxx",
+ "action_time": "xxx",
+ "feedback_collection": "xxx"
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/preferences/preferences/your_camera_roll_controls.json b/test/fixtures/facebook-json-2025-11-29/preferences/preferences/your_camera_roll_controls.json
new file mode 100644
index 0000000..c01cdaf
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/preferences/preferences/your_camera_roll_controls.json
@@ -0,0 +1,14 @@
+{
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/preferences/preferences/your_daily_limit.json b/test/fixtures/facebook-json-2025-11-29/preferences/preferences/your_daily_limit.json
new file mode 100644
index 0000000..d04c627
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/preferences/preferences/your_daily_limit.json
@@ -0,0 +1,15 @@
+{
+ "timestamp": 1711925000,
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "timestamp_value": 1712025000
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/preferences/preferences/your_device_push_settings.json b/test/fixtures/facebook-json-2025-11-29/preferences/preferences/your_device_push_settings.json
new file mode 100644
index 0000000..77ccbb7
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/preferences/preferences/your_device_push_settings.json
@@ -0,0 +1,30 @@
+[
+ {
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "timestamp_value": 1712040000
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+ },
+ {
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "timestamp_value": 1711435000
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+ }
+]
diff --git a/test/fixtures/facebook-json-2025-11-29/preferences/preferences/your_facebook_story_preferences.json b/test/fixtures/facebook-json-2025-11-29/preferences/preferences/your_facebook_story_preferences.json
new file mode 100644
index 0000000..c01cdaf
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/preferences/preferences/your_facebook_story_preferences.json
@@ -0,0 +1,14 @@
+{
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/preferences/preferences/your_post_composer_settings.json b/test/fixtures/facebook-json-2025-11-29/preferences/preferences/your_post_composer_settings.json
new file mode 100644
index 0000000..c01cdaf
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/preferences/preferences/your_post_composer_settings.json
@@ -0,0 +1,14 @@
+{
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/preferences/preferences/your_preferred_categories.json b/test/fixtures/facebook-json-2025-11-29/preferences/preferences/your_preferred_categories.json
new file mode 100644
index 0000000..44a22a0
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/preferences/preferences/your_preferred_categories.json
@@ -0,0 +1,35 @@
+{
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "vec": [
+ {
+ "dict": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ]
+ },
+ {
+ "dict": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/preferences/preferences/your_story_highlights.json b/test/fixtures/facebook-json-2025-11-29/preferences/preferences/your_story_highlights.json
new file mode 100644
index 0000000..c01cdaf
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/preferences/preferences/your_story_highlights.json
@@ -0,0 +1,14 @@
+{
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/preferences/preferences/your_video_accessibility_settings.json b/test/fixtures/facebook-json-2025-11-29/preferences/preferences/your_video_accessibility_settings.json
new file mode 100644
index 0000000..62a1cb5
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/preferences/preferences/your_video_accessibility_settings.json
@@ -0,0 +1,13 @@
+{
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/preferences/preferences/your_watch_settings.json b/test/fixtures/facebook-json-2025-11-29/preferences/preferences/your_watch_settings.json
new file mode 100644
index 0000000..c01cdaf
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/preferences/preferences/your_watch_settings.json
@@ -0,0 +1,14 @@
+{
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/security_and_login_information/account_activity.json b/test/fixtures/facebook-json-2025-11-29/security_and_login_information/account_activity.json
new file mode 100644
index 0000000..4adbed5
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/security_and_login_information/account_activity.json
@@ -0,0 +1,28 @@
+{
+ "account_activity_v2": [
+ {
+ "action": "xxx",
+ "timestamp": 1732270000,
+ "ip_address": "1.1.1.1",
+ "user_agent": "some/path",
+ "datr_cookie": "xxx",
+ "city": "xxx",
+ "region": "xxx",
+ "country": "xxx",
+ "site_name": "xxx",
+ "port": 69
+ },
+ {
+ "action": "xxx",
+ "timestamp": 1732230000,
+ "ip_address": "1.1.1.1",
+ "user_agent": "some/path",
+ "datr_cookie": "xxx",
+ "city": "xxx",
+ "region": "xxx",
+ "country": "xxx",
+ "site_name": "xxx",
+ "port": 69
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/security_and_login_information/browser_cookies.json b/test/fixtures/facebook-json-2025-11-29/security_and_login_information/browser_cookies.json
new file mode 100644
index 0000000..82abe31
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/security_and_login_information/browser_cookies.json
@@ -0,0 +1,7 @@
+{
+ "datr_stats_v2": {
+ "xxxxxx": [
+ 1707490000
+ ]
+ }
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/security_and_login_information/device_login_cookies.json b/test/fixtures/facebook-json-2025-11-29/security_and_login_information/device_login_cookies.json
new file mode 100644
index 0000000..44b4b63
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/security_and_login_information/device_login_cookies.json
@@ -0,0 +1,30 @@
+[
+ {
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "timestamp_value": 69
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+ },
+ {
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "timestamp_value": 69
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+ }
+]
diff --git a/test/fixtures/facebook-json-2025-11-29/security_and_login_information/email_address_verifications.json b/test/fixtures/facebook-json-2025-11-29/security_and_login_information/email_address_verifications.json
new file mode 100644
index 0000000..54b5247
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/security_and_login_information/email_address_verifications.json
@@ -0,0 +1,9 @@
+{
+ "contact_verifications_v2": [
+ {
+ "contact": "not_a_real_email@example.com",
+ "contact_type": 69,
+ "verification_time": 1707335000
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/security_and_login_information/information_about_your_last_login.json b/test/fixtures/facebook-json-2025-11-29/security_and_login_information/information_about_your_last_login.json
new file mode 100644
index 0000000..39cd07e
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/security_and_login_information/information_about_your_last_login.json
@@ -0,0 +1,21 @@
+{
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "timestamp_value": 1723300000
+ },
+ {
+ "label": "xxx",
+ "dict": [
+ {
+ "timestamp_value": 1707070000
+ },
+ {
+ "timestamp_value": 1723025000
+ }
+ ]
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/security_and_login_information/ip_address_activity.json b/test/fixtures/facebook-json-2025-11-29/security_and_login_information/ip_address_activity.json
new file mode 100644
index 0000000..ad74e40
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/security_and_login_information/ip_address_activity.json
@@ -0,0 +1,16 @@
+{
+ "used_ip_address_v2": [
+ {
+ "ip": "1.1.1.1",
+ "action": "xxx",
+ "timestamp": 1723300000,
+ "user_agent": "some/path"
+ },
+ {
+ "ip": "1.1.1.1",
+ "action": "xxx",
+ "timestamp": 1712260000,
+ "user_agent": "some/path"
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/security_and_login_information/login_protection_data.json b/test/fixtures/facebook-json-2025-11-29/security_and_login_information/login_protection_data.json
new file mode 100644
index 0000000..b27de0f
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/security_and_login_information/login_protection_data.json
@@ -0,0 +1,19 @@
+{
+ "login_protection_data_v2": [
+ {
+ "name": "xxx",
+ "session": {
+ "created_timestamp": 1711590000,
+ "updated_timestamp": 1730575000
+ }
+ },
+ {
+ "name": "xxx",
+ "session": {
+ "created_timestamp": 1724010000,
+ "updated_timestamp": 1730575000,
+ "ip_address": "1.1.1.1"
+ }
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/security_and_login_information/logins_and_logouts.json b/test/fixtures/facebook-json-2025-11-29/security_and_login_information/logins_and_logouts.json
new file mode 100644
index 0000000..6af44e8
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/security_and_login_information/logins_and_logouts.json
@@ -0,0 +1,16 @@
+{
+ "account_accesses_v2": [
+ {
+ "action": "xxx",
+ "timestamp": 1723300000,
+ "site": "xxx",
+ "ip_address": "1.1.1.1"
+ },
+ {
+ "action": "xxx",
+ "timestamp": 1723300000,
+ "site": "xxx",
+ "ip_address": "1.1.1.1"
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/security_and_login_information/record_details.json b/test/fixtures/facebook-json-2025-11-29/security_and_login_information/record_details.json
new file mode 100644
index 0000000..68d379b
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/security_and_login_information/record_details.json
@@ -0,0 +1,21 @@
+{
+ "admin_records_v2": [
+ {
+ "event": "xxx",
+ "session": {
+ "created_timestamp": 1723340000,
+ "user_agent": "some/path",
+ "datr_cookie": "xxx"
+ }
+ },
+ {
+ "event": "xxx",
+ "session": {
+ "created_timestamp": 1723300000,
+ "ip_address": "1.1.1.1",
+ "user_agent": "some/path",
+ "datr_cookie": "xxx"
+ }
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/security_and_login_information/registration_information.json b/test/fixtures/facebook-json-2025-11-29/security_and_login_information/registration_information.json
new file mode 100644
index 0000000..4297fbd
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/security_and_login_information/registration_information.json
@@ -0,0 +1,15 @@
+{
+ "timestamp": 1707465000,
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "not_a_real_email@example.com"
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/security_and_login_information/two-factor_authentication.json b/test/fixtures/facebook-json-2025-11-29/security_and_login_information/two-factor_authentication.json
new file mode 100644
index 0000000..97cca8c
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/security_and_login_information/two-factor_authentication.json
@@ -0,0 +1,14 @@
+{
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "timestamp_value": 1730135000
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/security_and_login_information/where_you're_logged_in.json b/test/fixtures/facebook-json-2025-11-29/security_and_login_information/where_you're_logged_in.json
new file mode 100644
index 0000000..e8a4114
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/security_and_login_information/where_you're_logged_in.json
@@ -0,0 +1,26 @@
+{
+ "active_sessions_v2": [
+ {
+ "created_timestamp": 1712260000,
+ "updated_timestamp": 1732330000,
+ "ip_address": "1.1.1.1",
+ "user_agent": "some/path",
+ "datr_cookie": "xxx",
+ "device": "xxx",
+ "location": "xxx",
+ "app": "xxx",
+ "session_type": "xxx"
+ },
+ {
+ "created_timestamp": 1712300000,
+ "updated_timestamp": 1732270000,
+ "ip_address": "1.1.1.1",
+ "user_agent": "some/path",
+ "datr_cookie": "xxx",
+ "device": "xxx",
+ "location": "xxx",
+ "app": "xxx",
+ "session_type": "xxx"
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/security_and_login_information/your_account_confirmation_information.json b/test/fixtures/facebook-json-2025-11-29/security_and_login_information/your_account_confirmation_information.json
new file mode 100644
index 0000000..c67c377
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/security_and_login_information/your_account_confirmation_information.json
@@ -0,0 +1,15 @@
+{
+ "timestamp": 1707465000,
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "timestamp_value": 69
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/security_and_login_information/your_facebook_activity_history.json b/test/fixtures/facebook-json-2025-11-29/security_and_login_information/your_facebook_activity_history.json
new file mode 100644
index 0000000..8f1099c
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/security_and_login_information/your_facebook_activity_history.json
@@ -0,0 +1,30 @@
+{
+ "last_activity_v2": {
+ "last_activity_time": {
+ "Website": {
+ "activity_by_day": [
+ 1732200000,
+ 1731340000
+ ]
+ },
+ "Facebook app": {
+ "activity_by_day": [
+ 1732030000,
+ 1731945000
+ ]
+ },
+ "Android": {
+ "activity_by_day": [
+ 1732030000,
+ 1731945000
+ ]
+ },
+ "Facebook Messenger for Android": {
+ "activity_by_day": [
+ 1730820000,
+ 1730735000
+ ]
+ }
+ }
+ }
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/comments_and_reactions/comments.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/comments_and_reactions/comments.json
new file mode 100644
index 0000000..aace103
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/comments_and_reactions/comments.json
@@ -0,0 +1,21 @@
+{
+ "comments_v2": [
+ {
+ "timestamp": 1707790000,
+ "title": "xxx"
+ },
+ {
+ "timestamp": 1720750000,
+ "data": [
+ {
+ "comment": {
+ "timestamp": 1720485000,
+ "comment": "xxx",
+ "author": "xxx"
+ }
+ }
+ ],
+ "title": "xxx"
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/comments_and_reactions/likes_and_reactions.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/comments_and_reactions/likes_and_reactions.json
new file mode 100644
index 0000000..33df8bd
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/comments_and_reactions/likes_and_reactions.json
@@ -0,0 +1,34 @@
+[
+ {
+ "timestamp": 1728680000,
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "url://somewhere",
+ "href": "url://somewhere"
+ }
+ ],
+ "fbid": "xxx"
+ },
+ {
+ "timestamp": 1712950000,
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "url://somewhere",
+ "href": "url://somewhere"
+ }
+ ],
+ "fbid": "xxx"
+ }
+]
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/comments_and_reactions/likes_and_reactions_1.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/comments_and_reactions/likes_and_reactions_1.json
new file mode 100644
index 0000000..46ba781
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/comments_and_reactions/likes_and_reactions_1.json
@@ -0,0 +1,26 @@
+[
+ {
+ "timestamp": 1707865000,
+ "data": [
+ {
+ "reaction": {
+ "reaction": "xxx",
+ "actor": "xxx"
+ }
+ }
+ ],
+ "title": "xxx"
+ },
+ {
+ "timestamp": 1709185000,
+ "data": [
+ {
+ "reaction": {
+ "reaction": "xxx",
+ "actor": "xxx"
+ }
+ }
+ ],
+ "title": "xxx"
+ }
+]
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/events/event_invitations.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/events/event_invitations.json
new file mode 100644
index 0000000..e5f7386
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/events/event_invitations.json
@@ -0,0 +1,14 @@
+{
+ "events_invited_v2": [
+ {
+ "name": "some/path",
+ "start_timestamp": 1723405000,
+ "end_timestamp": 1723420000
+ },
+ {
+ "name": "some/path",
+ "start_timestamp": 1721260000,
+ "end_timestamp": 1721270000
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/events/your_event_invitation_links.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/events/your_event_invitation_links.json
new file mode 100644
index 0000000..02bf686
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/events/your_event_invitation_links.json
@@ -0,0 +1,30 @@
+[
+ {
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+ },
+ {
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+ }
+]
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/events/your_event_responses.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/events/your_event_responses.json
new file mode 100644
index 0000000..d873a44
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/events/your_event_responses.json
@@ -0,0 +1,35 @@
+{
+ "event_responses_v2": {
+ "events_joined": [
+ {
+ "name": "xxx",
+ "start_timestamp": 1730805000,
+ "end_timestamp": 69
+ },
+ {
+ "name": "xxx",
+ "start_timestamp": 1733950000,
+ "end_timestamp": 69
+ }
+ ],
+ "events_declined": [
+ {
+ "name": "xxx",
+ "start_timestamp": 1713395000,
+ "end_timestamp": 69
+ }
+ ],
+ "events_interested": [
+ {
+ "name": "xxx",
+ "start_timestamp": 1729590000,
+ "end_timestamp": 1729615000
+ },
+ {
+ "name": "xxx",
+ "start_timestamp": 1704835000,
+ "end_timestamp": 1704855000
+ }
+ ]
+ }
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/events/your_events_ads_activity.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/events/your_events_ads_activity.json
new file mode 100644
index 0000000..2fd9c34
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/events/your_events_ads_activity.json
@@ -0,0 +1,10 @@
+{
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_gaming/bookmark_and_app_settings.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_gaming/bookmark_and_app_settings.json
new file mode 100644
index 0000000..1b605d2
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_gaming/bookmark_and_app_settings.json
@@ -0,0 +1,14 @@
+{
+ "timestamp": 1726760000,
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx"
+ },
+ {
+ "label": "xxx",
+ "timestamp_value": 69
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_gaming/instant_games_settings_and_metadata.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_gaming/instant_games_settings_and_metadata.json
new file mode 100644
index 0000000..6d066c5
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_gaming/instant_games_settings_and_metadata.json
@@ -0,0 +1,30 @@
+[
+ {
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "vec": []
+ }
+ ],
+ "fbid": "xxx"
+ },
+ {
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "vec": []
+ }
+ ],
+ "fbid": "xxx"
+ }
+]
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/conversations_you_had_as_a_buyer.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/conversations_you_had_as_a_buyer.json
new file mode 100644
index 0000000..9c93ae1
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/conversations_you_had_as_a_buyer.json
@@ -0,0 +1,32 @@
+[
+ {
+ "timestamp": 1730900000,
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "timestamp_value": 1730715000
+ }
+ ],
+ "fbid": "xxx"
+ },
+ {
+ "timestamp": 1730675000,
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "timestamp_value": 1730535000
+ }
+ ],
+ "fbid": "xxx"
+ }
+]
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/conversations_you_had_as_a_seller.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/conversations_you_had_as_a_seller.json
new file mode 100644
index 0000000..54a96e0
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/conversations_you_had_as_a_seller.json
@@ -0,0 +1,32 @@
+[
+ {
+ "timestamp": 1729840000,
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "timestamp_value": 1729580000
+ }
+ ],
+ "fbid": "xxx"
+ },
+ {
+ "timestamp": 1729680000,
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "timestamp_value": 1729435000
+ }
+ ],
+ "fbid": "xxx"
+ }
+]
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/for-sale_group_preferences.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/for-sale_group_preferences.json
new file mode 100644
index 0000000..ff93acc
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/for-sale_group_preferences.json
@@ -0,0 +1,30 @@
+[
+ {
+ "timestamp": 1729380000,
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "timestamp_value": 69
+ },
+ {
+ "label": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+ },
+ {
+ "timestamp": 1728835000,
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "timestamp_value": 69
+ },
+ {
+ "label": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+ }
+]
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/how_active_we_think_you_are_on_marketplace.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/how_active_we_think_you_are_on_marketplace.json
new file mode 100644
index 0000000..2fd9c34
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/how_active_we_think_you_are_on_marketplace.json
@@ -0,0 +1,10 @@
+{
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/items_sold.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/items_sold.json
new file mode 100644
index 0000000..b69d823
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/items_sold.json
@@ -0,0 +1,34 @@
+{
+ "items_selling_v2": [
+ {
+ "title": "xxx",
+ "price": "xxx",
+ "seller": "xxx",
+ "created_timestamp": 1727910000,
+ "category": "xxx",
+ "marketplace": "xxx",
+ "location": {
+ "coordinate": {
+ "latitude": 69,
+ "longitude": 69
+ }
+ },
+ "description": "xxx"
+ },
+ {
+ "title": "xxx",
+ "price": "xxx",
+ "seller": "xxx",
+ "created_timestamp": 1727400000,
+ "category": "xxx",
+ "marketplace": "xxx",
+ "location": {
+ "coordinate": {
+ "latitude": 69,
+ "longitude": 69
+ }
+ },
+ "description": "xxx"
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/marketplace_ratings_you've_given.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/marketplace_ratings_you've_given.json
new file mode 100644
index 0000000..29e06f7
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/marketplace_ratings_you've_given.json
@@ -0,0 +1,22 @@
+{
+ "ratings_v2": [
+ {
+ "timestamp": 1729860000,
+ "data": [
+ {
+ "text": "xxx"
+ }
+ ],
+ "title": "xxx"
+ },
+ {
+ "timestamp": 1727610000,
+ "data": [
+ {
+ "text": "xxx"
+ }
+ ],
+ "title": "xxx"
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/messages_received_on_a_thread.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/messages_received_on_a_thread.json
new file mode 100644
index 0000000..4792e91
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/messages_received_on_a_thread.json
@@ -0,0 +1,15 @@
+{
+ "timestamp": 1734920000,
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/messages_sent_on_a_thread.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/messages_sent_on_a_thread.json
new file mode 100644
index 0000000..4792e91
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/messages_sent_on_a_thread.json
@@ -0,0 +1,15 @@
+{
+ "timestamp": 1734920000,
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/offers_you_got.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/offers_you_got.json
new file mode 100644
index 0000000..533e245
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/offers_you_got.json
@@ -0,0 +1,32 @@
+[
+ {
+ "timestamp": 1713660000,
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+ },
+ {
+ "timestamp": 1717270000,
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+ }
+]
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/recent_searches_on_marketplace.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/recent_searches_on_marketplace.json
new file mode 100644
index 0000000..8ed3aea
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/recent_searches_on_marketplace.json
@@ -0,0 +1,48 @@
+[
+ {
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "vec": [
+ {
+ "dict": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "fbid": "xxx"
+ },
+ {
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "vec": [
+ {
+ "dict": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "fbid": "xxx"
+ }
+]
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/searches_you_have_repeated_in_marketplace.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/searches_you_have_repeated_in_marketplace.json
new file mode 100644
index 0000000..536c078
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/searches_you_have_repeated_in_marketplace.json
@@ -0,0 +1,37 @@
+{
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "dict": [
+ {
+ "label": "xxx",
+ "dict": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ]
+ },
+ {
+ "label": "xxx",
+ "dict": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/settings.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/settings.json
new file mode 100644
index 0000000..77a032c
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/settings.json
@@ -0,0 +1,15 @@
+{
+ "timestamp": 1707885000,
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "timestamp_value": 69
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/your_marketplace_assistant_settings.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/your_marketplace_assistant_settings.json
new file mode 100644
index 0000000..2fd9c34
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/your_marketplace_assistant_settings.json
@@ -0,0 +1,10 @@
+{
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/your_marketplace_cart_information.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/your_marketplace_cart_information.json
new file mode 100644
index 0000000..36d5d25
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/your_marketplace_cart_information.json
@@ -0,0 +1,15 @@
+{
+ "timestamp": 1722635000,
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "timestamp_value": 1716655000
+ },
+ {
+ "label": "xxx",
+ "timestamp_value": 69
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/your_marketplace_device_history.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/your_marketplace_device_history.json
new file mode 100644
index 0000000..c7178c1
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/your_marketplace_device_history.json
@@ -0,0 +1,22 @@
+[
+ {
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+ },
+ {
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+ }
+]
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/your_marketplace_items.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/your_marketplace_items.json
new file mode 100644
index 0000000..34506a9
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/your_marketplace_items.json
@@ -0,0 +1,15 @@
+{
+ "timestamp": 1719465000,
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/your_marketplace_listing_history.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/your_marketplace_listing_history.json
new file mode 100644
index 0000000..fc8b0fd
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/your_marketplace_listing_history.json
@@ -0,0 +1,30 @@
+[
+ {
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "timestamp_value": 1730725000
+ },
+ {
+ "label": "xxx",
+ "timestamp_value": 1730725000
+ }
+ ],
+ "fbid": "xxx"
+ },
+ {
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "timestamp_value": 1730635000
+ },
+ {
+ "label": "xxx",
+ "timestamp_value": 1730635000
+ }
+ ],
+ "fbid": "xxx"
+ }
+]
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/your_marketplace_listing_interactions.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/your_marketplace_listing_interactions.json
new file mode 100644
index 0000000..aacf897
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/your_marketplace_listing_interactions.json
@@ -0,0 +1,19 @@
+{
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "dict": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ]
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/your_marketplace_search_filters.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/your_marketplace_search_filters.json
new file mode 100644
index 0000000..9137d82
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/your_marketplace_search_filters.json
@@ -0,0 +1,15 @@
+{
+ "timestamp": 1723180000,
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "timestamp_value": 1723050000
+ },
+ {
+ "label": "xxx",
+ "timestamp_value": 1723050000
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/your_notification_status.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/your_notification_status.json
new file mode 100644
index 0000000..ff7d007
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/your_notification_status.json
@@ -0,0 +1,14 @@
+{
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "timestamp_value": 69
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/your_repeated_search_queries.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/your_repeated_search_queries.json
new file mode 100644
index 0000000..2fd9c34
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/your_repeated_search_queries.json
@@ -0,0 +1,10 @@
+{
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/your_search_history.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/your_search_history.json
new file mode 100644
index 0000000..1245095
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/your_search_history.json
@@ -0,0 +1,30 @@
+[
+ {
+ "timestamp": 1729835000,
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+ },
+ {
+ "timestamp": 1729080000,
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+ }
+]
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/your_structured_communication.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/your_structured_communication.json
new file mode 100644
index 0000000..f743848
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/your_structured_communication.json
@@ -0,0 +1,30 @@
+[
+ {
+ "timestamp": 1730900000,
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+ },
+ {
+ "timestamp": 1730675000,
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+ }
+]
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/your_transaction_survey_information.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/your_transaction_survey_information.json
new file mode 100644
index 0000000..2fd9c34
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_marketplace/your_transaction_survey_information.json
@@ -0,0 +1,10 @@
+{
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_payments/payment_history.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_payments/payment_history.json
new file mode 100644
index 0000000..c530965
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_payments/payment_history.json
@@ -0,0 +1,6 @@
+{
+ "payments_v2": {
+ "preferred_currency": "xxx",
+ "payments": []
+ }
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_payments/payments_you_have_made_as_a_buyer.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_payments/payments_you_have_made_as_a_buyer.json
new file mode 100644
index 0000000..b0ab9da
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_payments/payments_you_have_made_as_a_buyer.json
@@ -0,0 +1,30 @@
+[
+ {
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "timestamp_value": 1730635000
+ },
+ {
+ "label": "xxx",
+ "timestamp_value": 1730635000
+ }
+ ],
+ "fbid": "xxx"
+ },
+ {
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "timestamp_value": 1730410000
+ },
+ {
+ "label": "xxx",
+ "timestamp_value": 1730410000
+ }
+ ],
+ "fbid": "xxx"
+ }
+]
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_payments/payments_you_have_received_as_a_seller.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_payments/payments_you_have_received_as_a_seller.json
new file mode 100644
index 0000000..7a46568
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_payments/payments_you_have_received_as_a_seller.json
@@ -0,0 +1,30 @@
+[
+ {
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "timestamp_value": 1729575000
+ },
+ {
+ "label": "xxx",
+ "timestamp_value": 1729575000
+ }
+ ],
+ "fbid": "xxx"
+ },
+ {
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "timestamp_value": 1729415000
+ },
+ {
+ "label": "xxx",
+ "timestamp_value": 1729415000
+ }
+ ],
+ "fbid": "xxx"
+ }
+]
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_payments/your_payment_account_activity_history.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_payments/your_payment_account_activity_history.json
new file mode 100644
index 0000000..b0de380
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/facebook_payments/your_payment_account_activity_history.json
@@ -0,0 +1,15 @@
+{
+ "timestamp": 1718475000,
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/fundraisers/fundraiser_posts_you_likely_viewed.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/fundraisers/fundraiser_posts_you_likely_viewed.json
new file mode 100644
index 0000000..2fd9c34
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/fundraisers/fundraiser_posts_you_likely_viewed.json
@@ -0,0 +1,10 @@
+{
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/fundraisers/your_fundraiser_donations_information.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/fundraisers/your_fundraiser_donations_information.json
new file mode 100644
index 0000000..2fd9c34
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/fundraisers/your_fundraiser_donations_information.json
@@ -0,0 +1,10 @@
+{
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/fundraisers/your_fundraiser_settings.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/fundraisers/your_fundraiser_settings.json
new file mode 100644
index 0000000..d1adb8b
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/fundraisers/your_fundraiser_settings.json
@@ -0,0 +1,21 @@
+{
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "vec": [
+ {
+ "timestamp_value": 1706235000
+ },
+ {
+ "timestamp_value": 1706235000
+ }
+ ]
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/groups/chat_invites_received.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/groups/chat_invites_received.json
new file mode 100644
index 0000000..34e5cbf
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/groups/chat_invites_received.json
@@ -0,0 +1,32 @@
+[
+ {
+ "title": "xxx",
+ "timestamp": 1706830000,
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx"
+ },
+ {
+ "label": "xxx",
+ "timestamp_value": 1706570000
+ }
+ ],
+ "fbid": "xxx"
+ },
+ {
+ "title": "xxx",
+ "timestamp": 1735470000,
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx"
+ },
+ {
+ "label": "xxx",
+ "timestamp_value": 1705130000
+ }
+ ],
+ "fbid": "xxx"
+ }
+]
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/groups/community_chat_settings.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/groups/community_chat_settings.json
new file mode 100644
index 0000000..8fb3f9d
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/groups/community_chat_settings.json
@@ -0,0 +1,32 @@
+[
+ {
+ "title": "xxx",
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+ },
+ {
+ "title": "xxx",
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+ }
+]
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/groups/group_posts_and_comments.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/groups/group_posts_and_comments.json
new file mode 100644
index 0000000..e00932c
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/groups/group_posts_and_comments.json
@@ -0,0 +1,88 @@
+{
+ "group_posts_v2": [
+ {
+ "timestamp": 1728195000,
+ "attachments": [
+ {
+ "data": [
+ {
+ "media": {
+ "uri": "xxx",
+ "creation_timestamp": 1727405000,
+ "media_metadata": {
+ "photo_metadata": {
+ "exif_data": [
+ {
+ "iso": 69,
+ "focal_length": "some/path",
+ "upload_ip": "1.1.1.1",
+ "taken_timestamp": 1726270000,
+ "modified_timestamp": 1726270000,
+ "camera_make": "xxx",
+ "camera_model": "xxx",
+ "exposure": "some/path",
+ "f_stop": "some/path",
+ "orientation": 69,
+ "original_width": 69,
+ "original_height": 69
+ }
+ ]
+ }
+ },
+ "description": "xxx"
+ }
+ }
+ ]
+ }
+ ],
+ "data": [
+ {
+ "post": "xxx"
+ }
+ ],
+ "title": "xxx"
+ },
+ {
+ "timestamp": 1705940000,
+ "attachments": [
+ {
+ "data": [
+ {
+ "media": {
+ "uri": "xxx",
+ "creation_timestamp": 1705150000,
+ "media_metadata": {
+ "photo_metadata": {
+ "exif_data": [
+ {
+ "iso": 69,
+ "focal_length": "some/path",
+ "upload_ip": "1.1.1.1",
+ "taken_timestamp": 1704100000,
+ "modified_timestamp": 1704100000,
+ "camera_make": "xxx",
+ "camera_model": "xxx",
+ "exposure": "some/path",
+ "f_stop": "some/path",
+ "orientation": 69,
+ "original_width": 69,
+ "original_height": 69
+ }
+ ]
+ }
+ },
+ "description": "xxx"
+ }
+ }
+ ]
+ }
+ ],
+ "data": [
+ {
+ "post": "xxx"
+ }
+ ],
+ "title": "xxx"
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/groups/your_actions_on_violating_content_in_your_groups.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/groups/your_actions_on_violating_content_in_your_groups.json
new file mode 100644
index 0000000..87c13a6
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/groups/your_actions_on_violating_content_in_your_groups.json
@@ -0,0 +1,13 @@
+{
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx"
+ },
+ {
+ "label": "xxx",
+ "timestamp_value": 1728195000
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/groups/your_anonymous_mode_status_in_groups.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/groups/your_anonymous_mode_status_in_groups.json
new file mode 100644
index 0000000..068aa20
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/groups/your_anonymous_mode_status_in_groups.json
@@ -0,0 +1,28 @@
+[
+ {
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+ },
+ {
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+ }
+]
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/groups/your_answers_to_membership_questions.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/groups/your_answers_to_membership_questions.json
new file mode 100644
index 0000000..ccc19aa
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/groups/your_answers_to_membership_questions.json
@@ -0,0 +1,22 @@
+{
+ "group_membership_questions_answers_v2": {
+ "group_answers": [
+ {
+ "answers": [
+ {
+ "question": "xxx",
+ "answer": "xxx",
+ "timestamp": 1705680000
+ },
+ {
+ "question": "xxx",
+ "answer": "some/path",
+ "timestamp": 1705680000
+ }
+ ],
+ "rules_agreement": false,
+ "group_name": "xxx"
+ }
+ ]
+ }
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/groups/your_comments_in_groups.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/groups/your_comments_in_groups.json
new file mode 100644
index 0000000..13c51b1
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/groups/your_comments_in_groups.json
@@ -0,0 +1,12 @@
+{
+ "group_comments_v2": [
+ {
+ "timestamp": 1707750000,
+ "title": "xxx"
+ },
+ {
+ "timestamp": 1707780000,
+ "title": "xxx"
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/groups/your_contributions.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/groups/your_contributions.json
new file mode 100644
index 0000000..4066177
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/groups/your_contributions.json
@@ -0,0 +1,26 @@
+[
+ {
+ "title": "xxx",
+ "timestamp": 1729730000,
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "timestamp_value": 1729465000
+ }
+ ],
+ "fbid": "xxx"
+ },
+ {
+ "title": "xxx",
+ "timestamp": 1724365000,
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "timestamp_value": 1726100000
+ }
+ ],
+ "fbid": "xxx"
+ }
+]
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/groups/your_group_membership_activity.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/groups/your_group_membership_activity.json
new file mode 100644
index 0000000..003656d
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/groups/your_group_membership_activity.json
@@ -0,0 +1,22 @@
+{
+ "groups_joined_v2": [
+ {
+ "timestamp": 1707760000,
+ "data": [
+ {
+ "name": "xxx"
+ }
+ ],
+ "title": "xxx"
+ },
+ {
+ "timestamp": 1707760000,
+ "data": [
+ {
+ "name": "xxx"
+ }
+ ],
+ "title": "xxx"
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/groups/your_group_messages/00000000000.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/groups/your_group_messages/00000000000.json
new file mode 100644
index 0000000..8a5bd56
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/groups/your_group_messages/00000000000.json
@@ -0,0 +1,4 @@
+{
+ "thread_name": "xxx",
+ "messages": []
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/groups/your_group_shortcuts.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/groups/your_group_shortcuts.json
new file mode 100644
index 0000000..ad4d464
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/groups/your_group_shortcuts.json
@@ -0,0 +1,66 @@
+[
+ {
+ "timestamp": 1720105000,
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "vec": [
+ {
+ "value": "xxx"
+ },
+ {
+ "value": "xxx"
+ }
+ ]
+ },
+ {
+ "dict": [
+ {
+ "dict": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "title": "xxx"
+ }
+ ],
+ "title": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+ },
+ {
+ "timestamp": 1720105000,
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "vec": [
+ {
+ "value": "xxx"
+ },
+ {
+ "value": "xxx"
+ }
+ ]
+ },
+ {
+ "dict": [
+ {
+ "dict": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "title": "xxx"
+ }
+ ],
+ "title": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+ }
+]
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/groups/your_settings_for_groups_tab.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/groups/your_settings_for_groups_tab.json
new file mode 100644
index 0000000..c01cdaf
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/groups/your_settings_for_groups_tab.json
@@ -0,0 +1,14 @@
+{
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/messages/archived_threads/chatname_00000000000000000/message_1.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/messages/archived_threads/chatname_00000000000000000/message_1.json
new file mode 100644
index 0000000..0c2a134
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/messages/archived_threads/chatname_00000000000000000/message_1.json
@@ -0,0 +1,38 @@
+{
+ "participants": [
+ {
+ "name": "xxx"
+ },
+ {
+ "name": "xxx"
+ }
+ ],
+ "messages": [
+ {
+ "sender_name": "xxx",
+ "timestamp_ms": 69,
+ "content": "xxx",
+ "is_geoblocked_for_viewer": false,
+ "is_unsent_image_by_messenger_kid_parent": false
+ },
+ {
+ "sender_name": "xxx",
+ "timestamp_ms": 69,
+ "content": "xxx",
+ "is_geoblocked_for_viewer": false,
+ "is_unsent_image_by_messenger_kid_parent": false
+ }
+ ],
+ "title": "xxx",
+ "is_still_participant": true,
+ "thread_path": "some/path",
+ "magic_words": [],
+ "image": {
+ "uri": "your_facebook_activity/messages/photos/100x100.png",
+ "creation_timestamp": 1713680000
+ },
+ "joinable_mode": {
+ "mode": 69,
+ "link": "xxx"
+ }
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/messages/archived_threads/chatnametype2_000000000000000.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/messages/archived_threads/chatnametype2_000000000000000.json
new file mode 100644
index 0000000..7ef79e1
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/messages/archived_threads/chatnametype2_000000000000000.json
@@ -0,0 +1,45 @@
+{
+ "participants": [
+ {
+ "name": "xxx"
+ },
+ {
+ "name": "xxx"
+ }
+ ],
+ "messages": [
+ {
+ "sender_name": "xxx",
+ "timestamp_ms": 69,
+ "content": "xxx",
+ "reactions": [
+ {
+ "reaction": "xxx",
+ "actor": "xxx",
+ "timestamp": 1705570000
+ }
+ ],
+ "is_geoblocked_for_viewer": false,
+ "is_unsent_image_by_messenger_kid_parent": false
+ },
+ {
+ "sender_name": "xxx",
+ "timestamp_ms": 69,
+ "content": "some/path",
+ "is_geoblocked_for_viewer": false,
+ "is_unsent_image_by_messenger_kid_parent": false
+ }
+ ],
+ "title": "xxx",
+ "is_still_participant": true,
+ "thread_path": "some/path",
+ "magic_words": [],
+ "image": {
+ "uri": "your_facebook_activity/messages/photos/100x100.png",
+ "creation_timestamp": 1705020000
+ },
+ "joinable_mode": {
+ "mode": 69,
+ "link": "xxx"
+ }
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/messages/autofill_information.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/messages/autofill_information.json
new file mode 100644
index 0000000..387001a
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/messages/autofill_information.json
@@ -0,0 +1,26 @@
+{
+ "autofill_information_v2": {
+ "EMAIL": [
+ "not_a_real_email@example.com"
+ ],
+ "GENDER": [
+ "xxx"
+ ],
+ "SLIDER": [
+ "xxx",
+ "xxx"
+ ],
+ "FIRST_NAME": [
+ "xxx"
+ ],
+ "LAST_NAME": [
+ "xxx"
+ ],
+ "FULL_NAME": [
+ "xxx"
+ ],
+ "WORK_EMAIL": [
+ "not_a_real_email@example.com"
+ ]
+ }
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/messages/community_chats_settings.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/messages/community_chats_settings.json
new file mode 100644
index 0000000..c01cdaf
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/messages/community_chats_settings.json
@@ -0,0 +1,14 @@
+{
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/messages/e2ee_cutover/chatname_00000000000000000/message_1.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/messages/e2ee_cutover/chatname_00000000000000000/message_1.json
new file mode 100644
index 0000000..afe5322
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/messages/e2ee_cutover/chatname_00000000000000000/message_1.json
@@ -0,0 +1,30 @@
+{
+ "participants": [
+ {
+ "name": "xxx"
+ },
+ {
+ "name": "xxx"
+ }
+ ],
+ "messages": [
+ {
+ "sender_name": "xxx",
+ "timestamp_ms": 69,
+ "content": "xxx",
+ "is_geoblocked_for_viewer": false,
+ "is_unsent_image_by_messenger_kid_parent": false
+ },
+ {
+ "sender_name": "xxx",
+ "timestamp_ms": 69,
+ "content": "xxx",
+ "is_geoblocked_for_viewer": false,
+ "is_unsent_image_by_messenger_kid_parent": false
+ }
+ ],
+ "title": "xxx",
+ "is_still_participant": true,
+ "thread_path": "some/path",
+ "magic_words": []
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/messages/filtered_threads/chatname_000000000000000/message_1.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/messages/filtered_threads/chatname_000000000000000/message_1.json
new file mode 100644
index 0000000..bb4bbff
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/messages/filtered_threads/chatname_000000000000000/message_1.json
@@ -0,0 +1,22 @@
+{
+ "participants": [
+ {
+ "name": "xxx"
+ },
+ {
+ "name": "xxx"
+ }
+ ],
+ "messages": [
+ {
+ "sender_name": "xxx",
+ "timestamp_ms": 69,
+ "is_geoblocked_for_viewer": false,
+ "is_unsent_image_by_messenger_kid_parent": false
+ }
+ ],
+ "title": "xxx",
+ "is_still_participant": true,
+ "thread_path": "some/path",
+ "magic_words": []
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/messages/inbox/chatname_000000000000000000/message_1.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/messages/inbox/chatname_000000000000000000/message_1.json
new file mode 100644
index 0000000..b053efe
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/messages/inbox/chatname_000000000000000000/message_1.json
@@ -0,0 +1,35 @@
+{
+ "participants": [
+ {
+ "name": "xxx"
+ },
+ {
+ "name": "xxx"
+ }
+ ],
+ "messages": [
+ {
+ "sender_name": "xxx",
+ "timestamp_ms": 69,
+ "photos": [
+ {
+ "uri": "xxx",
+ "creation_timestamp": 1715665000
+ }
+ ],
+ "is_geoblocked_for_viewer": false,
+ "is_unsent_image_by_messenger_kid_parent": false
+ },
+ {
+ "sender_name": "xxx",
+ "timestamp_ms": 69,
+ "content": "xxx",
+ "is_geoblocked_for_viewer": false,
+ "is_unsent_image_by_messenger_kid_parent": false
+ }
+ ],
+ "title": "xxx",
+ "is_still_participant": true,
+ "thread_path": "some/path",
+ "magic_words": []
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/messages/messaging_settings.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/messages/messaging_settings.json
new file mode 100644
index 0000000..c01cdaf
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/messages/messaging_settings.json
@@ -0,0 +1,14 @@
+{
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/messages/messenger_active_status_platform_settings.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/messages/messenger_active_status_platform_settings.json
new file mode 100644
index 0000000..c01cdaf
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/messages/messenger_active_status_platform_settings.json
@@ -0,0 +1,14 @@
+{
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/messages/messenger_active_status_settings.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/messages/messenger_active_status_settings.json
new file mode 100644
index 0000000..c01cdaf
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/messages/messenger_active_status_settings.json
@@ -0,0 +1,14 @@
+{
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/messages/messenger_ui_settings.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/messages/messenger_ui_settings.json
new file mode 100644
index 0000000..c01cdaf
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/messages/messenger_ui_settings.json
@@ -0,0 +1,14 @@
+{
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/messages/photos/100x100.png b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/messages/photos/100x100.png
new file mode 100644
index 0000000..abc6b4a
Binary files /dev/null and b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/messages/photos/100x100.png differ
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/messages/secret_conversations.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/messages/secret_conversations.json
new file mode 100644
index 0000000..7325766
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/messages/secret_conversations.json
@@ -0,0 +1,24 @@
+{
+ "secret_conversations_v2": {
+ "tincan_devices": [],
+ "armadillo_devices": [
+ {
+ "device_type": "xxx",
+ "device_manufacturer": "xxx",
+ "device_model": "xxx",
+ "device_os_version": "xxx",
+ "last_connected_ip": "1.1.1.1",
+ "last_active_time": 1732235000
+ },
+ {
+ "device_type": "xxx",
+ "device_manufacturer": "xxx",
+ "device_model": "xxx",
+ "device_os_version": "xxx",
+ "last_connected_ip": "1.1.1.1",
+ "last_active_time": 1732330000
+ }
+ ],
+ "calls": []
+ }
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/messages/support_messages.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/messages/support_messages.json
new file mode 100644
index 0000000..1fac310
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/messages/support_messages.json
@@ -0,0 +1,51 @@
+{
+ "support_messages": {
+ "000000000000000000": {
+ "timestamp": 1713675000,
+ "subject": "xxx",
+ "messages": [
+ {
+ "from": "xxx",
+ "to": "xxx",
+ "subject": "xxx",
+ "message": "xxx",
+ "timestamp": 1713540000,
+ "attachments": []
+ }
+ ]
+ },
+ "000000000000000001": {
+ "timestamp": 1724745000,
+ "subject": "xxx",
+ "messages": [
+ {
+ "from": "xxx",
+ "to": "xxx",
+ "subject": "xxx",
+ "message": "xxx",
+ "timestamp": 1724610000
+ },
+ {
+ "from": "xxx",
+ "to": "xxx",
+ "subject": "xxx",
+ "message": "xxx",
+ "timestamp": 1724610000
+ }
+ ]
+ },
+ "000000000000000002": {
+ "timestamp": 1713675000,
+ "subject": "xxx",
+ "messages": [
+ {
+ "from": "xxx",
+ "to": "xxx",
+ "subject": "xxx",
+ "message": "xxx",
+ "timestamp": 1713540000
+ }
+ ]
+ }
+ }
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/messages/your_chat_settings_on_web.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/messages/your_chat_settings_on_web.json
new file mode 100644
index 0000000..c01cdaf
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/messages/your_chat_settings_on_web.json
@@ -0,0 +1,14 @@
+{
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/messages/your_messenger_app_install_information.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/messages/your_messenger_app_install_information.json
new file mode 100644
index 0000000..c01cdaf
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/messages/your_messenger_app_install_information.json
@@ -0,0 +1,14 @@
+{
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/messages/your_responsiveness_in_messaging_threads.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/messages/your_responsiveness_in_messaging_threads.json
new file mode 100644
index 0000000..8fe0992
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/messages/your_responsiveness_in_messaging_threads.json
@@ -0,0 +1,22 @@
+[
+ {
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "timestamp_value": 1729575000
+ }
+ ],
+ "fbid": "xxx"
+ },
+ {
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "timestamp_value": 1729415000
+ }
+ ],
+ "fbid": "xxx"
+ }
+]
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/navigation_bar/device_navigation_bar_information.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/navigation_bar/device_navigation_bar_information.json
new file mode 100644
index 0000000..c6a7eb7
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/navigation_bar/device_navigation_bar_information.json
@@ -0,0 +1,80 @@
+[
+ {
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "vec": [
+ {
+ "dict": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "timestamp_value": 1705495000
+ }
+ ]
+ },
+ {
+ "dict": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "timestamp_value": 1705495000
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+ },
+ {
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "vec": [
+ {
+ "dict": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "timestamp_value": 1710490000
+ }
+ ]
+ },
+ {
+ "dict": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "timestamp_value": 1710490000
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+ }
+]
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/navigation_bar/navigation_bar_shortcut_history.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/navigation_bar/navigation_bar_shortcut_history.json
new file mode 100644
index 0000000..c978bb8
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/navigation_bar/navigation_bar_shortcut_history.json
@@ -0,0 +1,58 @@
+[
+ {
+ "timestamp": 1732330000,
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "vec": [
+ {
+ "dict": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ]
+ },
+ {
+ "dict": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "fbid": "xxx"
+ },
+ {
+ "timestamp": 1730880000,
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "vec": [
+ {
+ "dict": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ]
+ },
+ {
+ "dict": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "fbid": "xxx"
+ }
+]
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/navigation_bar/notification_tab_display_information.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/navigation_bar/notification_tab_display_information.json
new file mode 100644
index 0000000..9c2ab45
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/navigation_bar/notification_tab_display_information.json
@@ -0,0 +1,15 @@
+{
+ "timestamp": 1708500000,
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "timestamp_value": 69
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/navigation_bar/your_tab_notifications.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/navigation_bar/your_tab_notifications.json
new file mode 100644
index 0000000..e19f121
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/navigation_bar/your_tab_notifications.json
@@ -0,0 +1,14 @@
+{
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "timestamp_value": 1729260000
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/other_activity/facebook_new_user_guide.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/other_activity/facebook_new_user_guide.json
new file mode 100644
index 0000000..79c4505
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/other_activity/facebook_new_user_guide.json
@@ -0,0 +1,28 @@
+[
+ {
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+ },
+ {
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+ }
+]
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/other_activity/statistics_on_your_recent_messenger_activity.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/other_activity/statistics_on_your_recent_messenger_activity.json
new file mode 100644
index 0000000..5b14430
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/other_activity/statistics_on_your_recent_messenger_activity.json
@@ -0,0 +1,14 @@
+{
+ "media": [],
+ "label_values": [
+ {
+ "label": "some/path",
+ "value": "xxx"
+ },
+ {
+ "label": "some/path",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/other_activity/time_spent_on_facebook.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/other_activity/time_spent_on_facebook.json
new file mode 100644
index 0000000..2f8c5af
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/other_activity/time_spent_on_facebook.json
@@ -0,0 +1,49 @@
+{
+ "timestamp": 1725585000,
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "vec": [
+ {
+ "dict": [
+ {
+ "label": "xxx",
+ "timestamp_value": 1731270000
+ },
+ {
+ "label": "xxx",
+ "timestamp_value": 1731270000
+ }
+ ]
+ },
+ {
+ "dict": [
+ {
+ "label": "xxx",
+ "timestamp_value": 1731270000
+ },
+ {
+ "label": "xxx",
+ "timestamp_value": 1731270000
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "label": "xxx",
+ "dict": [
+ {
+ "label": "xxx",
+ "value": "some/path"
+ },
+ {
+ "label": "xxx",
+ "value": "some/path"
+ }
+ ]
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/other_activity/weather_forecast_settings.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/other_activity/weather_forecast_settings.json
new file mode 100644
index 0000000..df9a24e
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/other_activity/weather_forecast_settings.json
@@ -0,0 +1,14 @@
+{
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "timestamp_value": 69
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/other_activity/your_autofill_settings.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/other_activity/your_autofill_settings.json
new file mode 100644
index 0000000..cb2da69
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/other_activity/your_autofill_settings.json
@@ -0,0 +1,41 @@
+{
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "dict": [
+ {
+ "label": "xxx",
+ "dict": [
+ {
+ "label": "xxx",
+ "timestamp_value": 1723535000
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ]
+ },
+ {
+ "label": "xxx",
+ "dict": [
+ {
+ "label": "xxx",
+ "timestamp_value": 69
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/other_activity/your_content_visibility_notification_history.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/other_activity/your_content_visibility_notification_history.json
new file mode 100644
index 0000000..c01cdaf
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/other_activity/your_content_visibility_notification_history.json
@@ -0,0 +1,14 @@
+{
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/other_activity/your_crowdsourcing_edits.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/other_activity/your_crowdsourcing_edits.json
new file mode 100644
index 0000000..a1cbfd6
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/other_activity/your_crowdsourcing_edits.json
@@ -0,0 +1,30 @@
+[
+ {
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "timestamp_value": 1723030000
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+ },
+ {
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "timestamp_value": 1723030000
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+ }
+]
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/other_activity/your_information_download_requests.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/other_activity/your_information_download_requests.json
new file mode 100644
index 0000000..4239fb7
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/other_activity/your_information_download_requests.json
@@ -0,0 +1,32 @@
+[
+ {
+ "timestamp": 1717500000,
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "timestamp_value": 1717260000
+ },
+ {
+ "label": "xxx",
+ "timestamp_value": 1717235000
+ }
+ ],
+ "fbid": "xxx"
+ },
+ {
+ "timestamp": 1705005000,
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "timestamp_value": 1704745000
+ },
+ {
+ "label": "xxx",
+ "timestamp_value": 1704745000
+ }
+ ],
+ "fbid": "xxx"
+ }
+]
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/other_activity/your_interactions_with_promotional_content_in_reels.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/other_activity/your_interactions_with_promotional_content_in_reels.json
new file mode 100644
index 0000000..6f994f4
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/other_activity/your_interactions_with_promotional_content_in_reels.json
@@ -0,0 +1,14 @@
+{
+ "timestamp": 69,
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/other_activity/your_link_history_settings.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/other_activity/your_link_history_settings.json
new file mode 100644
index 0000000..e1e25d2
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/other_activity/your_link_history_settings.json
@@ -0,0 +1,14 @@
+{
+ "timestamp": 1735220000,
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx"
+ },
+ {
+ "label": "xxx",
+ "timestamp_value": 1716420000
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/other_activity/your_mentions_settings.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/other_activity/your_mentions_settings.json
new file mode 100644
index 0000000..5a2accd
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/other_activity/your_mentions_settings.json
@@ -0,0 +1,15 @@
+{
+ "timestamp": 1713515000,
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/other_activity/your_recently_used_emojis.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/other_activity/your_recently_used_emojis.json
new file mode 100644
index 0000000..61445fe
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/other_activity/your_recently_used_emojis.json
@@ -0,0 +1,24 @@
+{
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "dict": [
+ {
+ "label": "xxx",
+ "dict": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "timestamp_value": 1715555000
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/other_activity/your_video_consumption_summary.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/other_activity/your_video_consumption_summary.json
new file mode 100644
index 0000000..c01cdaf
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/other_activity/your_video_consumption_summary.json
@@ -0,0 +1,14 @@
+{
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/pages/pages_and_profiles_you've_unfollowed.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/pages/pages_and_profiles_you've_unfollowed.json
new file mode 100644
index 0000000..7246d4f
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/pages/pages_and_profiles_you've_unfollowed.json
@@ -0,0 +1,13 @@
+{
+ "pages_unfollowed_v2": [
+ {
+ "timestamp": 1708485000,
+ "data": [
+ {
+ "name": "xxx"
+ }
+ ],
+ "title": "xxx"
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/pages/pages_you_are_a_customer_of.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/pages/pages_you_are_a_customer_of.json
new file mode 100644
index 0000000..c075693
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/pages/pages_you_are_a_customer_of.json
@@ -0,0 +1,30 @@
+[
+ {
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "timestamp_value": 69
+ }
+ ],
+ "fbid": "xxx"
+ },
+ {
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ },
+ {
+ "label": "xxx",
+ "timestamp_value": 69
+ }
+ ],
+ "fbid": "xxx"
+ }
+]
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/pages/your_pages.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/pages/your_pages.json
new file mode 100644
index 0000000..55c9c9d
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/pages/your_pages.json
@@ -0,0 +1,9 @@
+{
+ "pages_v2": [
+ {
+ "name": "xxx",
+ "timestamp": 1723290000,
+ "url": "url://somewhere"
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/posts/album/0.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/posts/album/0.json
new file mode 100644
index 0000000..bd7d185
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/posts/album/0.json
@@ -0,0 +1,37 @@
+{
+ "name": "xxx",
+ "photos": [
+ {
+ "uri": "your_facebook_activity/posts/media/Album_000000000000/100x100.png",
+ "creation_timestamp": 1707740000,
+ "media_metadata": {
+ "photo_metadata": {
+ "exif_data": [
+ {
+ "upload_ip": "1.1.1.1",
+ "taken_timestamp": 1707475000
+ }
+ ]
+ }
+ },
+ "title": "xxx"
+ }
+ ],
+ "cover_photo": {
+ "uri": "your_facebook_activity/posts/media/Album_000000000000/100x100.png",
+ "creation_timestamp": 1707870000,
+ "media_metadata": {
+ "photo_metadata": {
+ "exif_data": [
+ {
+ "upload_ip": "1.1.1.1",
+ "taken_timestamp": 1707740000
+ }
+ ]
+ }
+ },
+ "title": "xxx"
+ },
+ "last_modified_timestamp": 1707870000,
+ "description": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/posts/content_sharing_links_you_have_created.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/posts/content_sharing_links_you_have_created.json
new file mode 100644
index 0000000..4e56be1
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/posts/content_sharing_links_you_have_created.json
@@ -0,0 +1,36 @@
+[
+ {
+ "timestamp": 1728680000,
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "url://somewhere",
+ "href": "url://somewhere"
+ },
+ {
+ "label": "xxx",
+ "value": "url://somewhere",
+ "href": "url://somewhere"
+ }
+ ],
+ "fbid": "xxx"
+ },
+ {
+ "timestamp": 1727685000,
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "url://somewhere",
+ "href": "url://somewhere"
+ },
+ {
+ "label": "xxx",
+ "value": "url://somewhere",
+ "href": "url://somewhere"
+ }
+ ],
+ "fbid": "xxx"
+ }
+]
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/posts/edits_you_made_to_posts.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/posts/edits_you_made_to_posts.json
new file mode 100644
index 0000000..531ae99
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/posts/edits_you_made_to_posts.json
@@ -0,0 +1,24 @@
+[
+ {
+ "timestamp": 1727910000,
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+ },
+ {
+ "timestamp": 1727400000,
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+ }
+]
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/posts/media/Album_000000000000/100x100.png b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/posts/media/Album_000000000000/100x100.png
new file mode 100644
index 0000000..abc6b4a
Binary files /dev/null and b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/posts/media/Album_000000000000/100x100.png differ
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/posts/visual_search_on_your_posts.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/posts/visual_search_on_your_posts.json
new file mode 100644
index 0000000..2fd9c34
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/posts/visual_search_on_your_posts.json
@@ -0,0 +1,10 @@
+{
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/posts/your_posts__check_ins__photos_and_videos_1.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/posts/your_posts__check_ins__photos_and_videos_1.json
new file mode 100644
index 0000000..1c27f5a
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/posts/your_posts__check_ins__photos_and_videos_1.json
@@ -0,0 +1,26 @@
+{
+ "timestamp": 1707465000,
+ "attachments": [
+ {
+ "data": [
+ {
+ "life_event": {
+ "title": "xxx",
+ "start_date": {
+ "year": 69,
+ "month": 69,
+ "day": 69
+ }
+ }
+ }
+ ]
+ }
+ ],
+ "data": [
+ {
+ "backdated_timestamp": 69
+ },
+ {}
+ ],
+ "title": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/posts/your_uncategorized_photos.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/posts/your_uncategorized_photos.json
new file mode 100644
index 0000000..274162a
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/posts/your_uncategorized_photos.json
@@ -0,0 +1,32 @@
+{
+ "other_photos_v2": [
+ {
+ "uri": "xxx",
+ "creation_timestamp": 1722435000,
+ "media_metadata": {
+ "photo_metadata": {
+ "exif_data": [
+ {
+ "upload_ip": "1.1.1.1",
+ "taken_timestamp": 1722170000
+ }
+ ]
+ }
+ }
+ },
+ {
+ "uri": "xxx",
+ "creation_timestamp": 1722435000,
+ "media_metadata": {
+ "photo_metadata": {
+ "exif_data": [
+ {
+ "upload_ip": "1.1.1.1",
+ "taken_timestamp": 1722170000
+ }
+ ]
+ }
+ }
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/posts/your_videos.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/posts/your_videos.json
new file mode 100644
index 0000000..d749fa2
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/posts/your_videos.json
@@ -0,0 +1,38 @@
+{
+ "videos_v2": [
+ {
+ "uri": "xxx",
+ "creation_timestamp": 1715670000,
+ "media_metadata": {
+ "video_metadata": {
+ "exif_data": [
+ {
+ "upload_ip": "1.1.1.1",
+ "upload_timestamp": 69
+ }
+ ]
+ }
+ },
+ "description": "xxx",
+ "dubbing_info": [],
+ "media_variants": []
+ },
+ {
+ "uri": "xxx",
+ "creation_timestamp": 1708345000,
+ "media_metadata": {
+ "video_metadata": {
+ "exif_data": [
+ {
+ "upload_ip": "1.1.1.1",
+ "upload_timestamp": 69
+ }
+ ]
+ }
+ },
+ "description": "xxx",
+ "dubbing_info": [],
+ "media_variants": []
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/saved_items_and_collections/collections.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/saved_items_and_collections/collections.json
new file mode 100644
index 0000000..1d57b8d
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/saved_items_and_collections/collections.json
@@ -0,0 +1,30 @@
+{
+ "collections_v2": [
+ {
+ "timestamp": 1722010000,
+ "attachments": [
+ {
+ "data": [
+ {
+ "name": "xxx"
+ }
+ ]
+ }
+ ],
+ "title": "xxx"
+ },
+ {
+ "timestamp": 1721580000,
+ "attachments": [
+ {
+ "data": [
+ {
+ "name": "xxx"
+ }
+ ]
+ }
+ ],
+ "title": "xxx"
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/saved_items_and_collections/your_saved_items.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/saved_items_and_collections/your_saved_items.json
new file mode 100644
index 0000000..28dca75
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/saved_items_and_collections/your_saved_items.json
@@ -0,0 +1,36 @@
+{
+ "saves_v2": [
+ {
+ "timestamp": 1722010000,
+ "attachments": [
+ {
+ "data": [
+ {
+ "event": {
+ "name": "xxx",
+ "start_timestamp": 1721380000,
+ "end_timestamp": 69
+ }
+ }
+ ]
+ }
+ ],
+ "title": "xxx"
+ },
+ {
+ "timestamp": 1723630000,
+ "attachments": [
+ {
+ "data": [
+ {
+ "external_context": {
+ "name": "xxx"
+ }
+ }
+ ]
+ }
+ ],
+ "title": "xxx"
+ }
+ ]
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/shops/your_recently_viewed_products.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/shops/your_recently_viewed_products.json
new file mode 100644
index 0000000..2a65c42
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/shops/your_recently_viewed_products.json
@@ -0,0 +1,17 @@
+{
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "vec": [
+ {
+ "value": "xxx"
+ },
+ {
+ "value": "xxx"
+ }
+ ]
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/voting/voting_location.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/voting/voting_location.json
new file mode 100644
index 0000000..6d5ec8e
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/voting/voting_location.json
@@ -0,0 +1,5 @@
+{
+ "voting_location_v2": {
+ "voting_location": "xxx"
+ }
+}
diff --git a/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/voting/voting_reminders.json b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/voting/voting_reminders.json
new file mode 100644
index 0000000..2fd9c34
--- /dev/null
+++ b/test/fixtures/facebook-json-2025-11-29/your_facebook_activity/voting/voting_reminders.json
@@ -0,0 +1,10 @@
+{
+ "media": [],
+ "label_values": [
+ {
+ "label": "xxx",
+ "value": "xxx"
+ }
+ ],
+ "fbid": "xxx"
+}
diff --git a/test/task.ts b/test/task.ts
new file mode 100644
index 0000000..22d0519
--- /dev/null
+++ b/test/task.ts
@@ -0,0 +1,10 @@
+import test from "node:test";
+import fs from "node:fs";
+import assert from "node:assert";
+import { finished } from "node:stream/promises";
+import { Readable, Writable } from "node:stream";
+import { TaskTargetPipelineHelper } from "../data-export/task.ts";
+
+test("facebook: Can load the 2021 export", async () => {
+
+});
\ No newline at end of file
diff --git a/test/util.ts b/test/util.ts
deleted file mode 100644
index de63241..0000000
--- a/test/util.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import test from "node:test";
-import fs from "node:fs";
-import assert from "node:assert";
-import { finished } from "node:stream/promises";
-import { Readable, Writable } from "node:stream";
-import { ChildProcessDuplex } from "../data-export/util.ts"; // <-- adjust path
-
-// Helper: create a short Readable to feed stdin
-const smallInput = () =>
- Readable.from(["hello\n", "world\n"], { objectMode: false });
-
-// Helper for timing out promises
-function timeout(p: Promise, ms: number): Promise {
- return new Promise((resolve, reject) => {
- const t = setTimeout(() => reject(new Error("timeout")), ms);
- p.then(v => { clearTimeout(t); resolve(v); }, err => { clearTimeout(t); reject(err); });
- });
-}
-
-test("ChildProcessDuplex: command that exits at same time as stream close should work with finished()", async () => {
- const child = new ChildProcessDuplex(
- "sh",
- ["-c", "cat >/dev/null"]
- );
-
- const s = smallInput().pipe(child).pipe(fs.createWriteStream("/dev/null"));
-
- await timeout(finished(s), 500);
-});
-test("ChildProcessDuplex: command that exits after delay after stream closes should work with finished()", async () => {
- const child = new ChildProcessDuplex(
- "sh",
- ["-c", "cat >/dev/null; sleep 0.5"]
- );
-
- // This WILL NOT WORK unless ChildProcessDuplex emits "close" when the underlying ChildProcess emits "close"
- // but somehow the previous test will work
- const s = smallInput().pipe(child);
- await timeout(finished(s), 500);
-});
diff --git a/util/ignore-css-loader.js b/util/ignore-css-loader.js
deleted file mode 100644
index 083e9af..0000000
--- a/util/ignore-css-loader.js
+++ /dev/null
@@ -1,54 +0,0 @@
-import { isMainThread } from 'node:worker_threads';
-import module from 'node:module';
-import path from 'node:path';
-
-
-/**
- * Remove query params and hashes from a file path or URL string.
- * @param {string} f The source to strip.
- */
-export function stripExtras(f) {
- return f.split('?')[0].split('#')[0];
-}
-
-/** @typedef {import('../types.d.ts').FileURL} FileURL */
-
-/**
- * @type {import('node:module').ResolveHook}
- */
-async function resolveCSS(specifier, ctx, nextResolve) {
- const nextResult = await nextResolve(specifier);
- // Check against the fully resolved URL, not just the specifier, in case another loader has
- // something to contribute to the resolution.
- const ext = path.extname(stripExtras(/** @type {FileURL} */ (nextResult.url)));
-
- if (ext === '.css') {
- return {
- ...nextResult,
- format: 'css',
- };
- }
-
- return nextResult;
-}
-export { resolveCSS as resolve };
-
-/**
- * @type {import('node:module').LoadHook}
- * @param {FileURL} url The fully resolved url.
- */
-async function loadCSS(url, ctx, nextLoad) {
- if (ctx.format !== 'css') {
- return nextLoad(url);
- }
- await nextLoad(url);
- return {
- format: "module",
- source: "",
- };
-}
-export { loadCSS as load };
-
-if (isMainThread && 'register' in module) {
- module.register('./ignore-css-loader.js', import.meta.url);
-}
diff --git a/util/scrub.jq b/util/scrub.jq
new file mode 100644
index 0000000..887d02b
--- /dev/null
+++ b/util/scrub.jq
@@ -0,0 +1,48 @@
+# Use this to process json files before loading them into unit tests
+# Something like:
+# fd -t f .json -0 | xargs -I % -0 -- jq -f scrub.jq "%" > "%"
+# (Though you should remove the end `> "%"` first to get just the output without
+# persisting to be sure it's what you want first)
+def scrub:
+ walk(
+ if type == "string" then
+ if test("^(([0-9]{1,3}\\.){3}[0-9]{1,3})$") then
+ "1.1.1.1"
+ elif test("^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$") then
+ "2000:0000:0000:0000:0000:0000:0000:0000"
+ elif test("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$") then
+ "not_a_real_email@example.com"
+ elif test("\\.(jpg|jpeg|png|gif|bmp|webp|svg|ico|tiff|mp3|wav|flac|aac|ogg|wma|m4a|mp4|avi|mkv|mov|wmv|flv|webm)$"; "i") then
+ # Leave these alone, you will have to manually go through these later and replace with
+ # placeholders
+ # TODO: jq 1.7 adds debug(), use this instead when I can upgrade jq, otherwise
+ # you need to manually grep for MANUAL REPAIR NEEDED for now
+ ("MANUAL REPAIR NEEDED: \(.)" | stderr) | .
+ elif test("://") then
+ "url://somewhere"
+ elif test("/") then
+ "some/path"
+ else
+ "xxx"
+ end
+ elif type == "number" then
+ if 946702800 <= . and . <= 1893474000 then
+ # Take modulo 1 year to get variance in the output, then add offset to bring to ~2024
+ ((((. % 31557600) + 1704067200) / 5000 | floor) * 5000)
+ else
+ 69
+ end
+ elif type == "array" then
+ # Keep only 2 elements, but scrub *those* elements
+ if length > 1 then
+ [ (.[0] | scrub), (.[1] | scrub) ]
+ elif length > 0 then
+ [ (.[0] | scrub) ]
+ else
+ []
+ end
+ else
+ .
+ end
+ );
+scrub
\ No newline at end of file
diff --git a/util/scrub.ts b/util/scrub.ts
new file mode 100755
index 0000000..07024c4
--- /dev/null
+++ b/util/scrub.ts
@@ -0,0 +1,61 @@
+#!/usr/bin/env -S node
+
+import { $, argv, path } from "zx";
+import { strict as assert } from "node:assert";
+import fs from "node:fs/promises";
+
+/**Catches any p Promise throws and instead returns those in a tuple*/
+async function ptry(
+ p: Promise
+): Promise<[TError, undefined] | [undefined, TRet]> {
+ try {
+ const result = await p;
+ return [undefined, result];
+ } catch (err) {
+ return [err as TError, undefined];
+ }
+}
+
+$.verbose = true;
+
+const scriptDir = path.dirname(new URL(import.meta.url).pathname);
+const scrubJq = path.join(scriptDir, "scrub.jq");
+
+const targetDir = argv._[0];
+
+assert(targetDir, "Usage: ./scrub.ts ");
+
+const targetPath = path.resolve(targetDir);
+
+// const stat = await fs.stat(targetPath);
+// assert(stat.isDirectory(), "");
+
+const [notADir] = await ptry($`test -d ${targetPath}`);
+assert(!notADir, `Error: '${targetPath}' is not a directory`);
+
+const [noScrubJq] = await ptry($`test -f ${scrubJq}`);
+assert(!noScrubJq, `Error: scrub.jq not found at ${scrubJq}`);
+
+console.log(`Scrubbing JSON files in: ${targetPath}`);
+console.log(`Using scrub.jq from: ${scrubJq}`);
+console.log();
+
+const [findErr, files] = await ptry($`fdfind -t f '\\.json$' ${targetPath} -0`.quiet());
+assert(!findErr, `Error finding JSON files: ${findErr}`);
+
+const filePaths = files.stdout.split("\0").filter(Boolean);
+console.log("filePaths", filePaths);
+
+for (const file of filePaths) {
+ console.log(`Processing: ${file}`);
+ const tmpFile = `${file}.tmp`;
+
+ const [jqErr] = await ptry($`jq -f ${scrubJq} ${file} > ${tmpFile}`);
+ assert(!jqErr, `Error processing ${file}: ${jqErr}`);
+
+ const [mvErr] = await ptry($`mv ${tmpFile} ${file}`);
+ assert(!mvErr, `Error moving ${tmpFile} to ${file}: ${mvErr}`);
+}
+
+console.log();
+console.log("Done!");
\ No newline at end of file
diff --git a/util/tsx-loader.js b/util/tsx-loader.js
deleted file mode 100644
index b3003a6..0000000
--- a/util/tsx-loader.js
+++ /dev/null
@@ -1,116 +0,0 @@
-import { isMainThread } from 'node:worker_threads';
-import module from 'node:module';
-import path from 'node:path';
-import { cwd } from 'node:process';
-import { pathToFileURL } from 'node:url';
-import { transform } from 'esbuild';
-
-/**
- * Remove query params and hashes from a file path or URL string.
- * @param {string} f The source to strip.
- */
-export function stripExtras(f) {
- return f.split('?')[0].split('#')[0];
-}
-
-/** @typedef {import('../types.d.ts').FileURL} FileURL */
-
-/**
- * The load hook needs to know the parent URL to find the esbuild config.
- * But load hooks don't have access to the parent URL.
- * If you try to pass it as return value from the resolve hook, it will be overwritten by node.
- *
- * @type {Map}
- */
-export const parentURLs = new Map();
-
-/**
- * @type {import('node:module').ResolveHook}
- */
-async function resolveTSX(specifier, ctx, nextResolve) {
- const nextResult = await nextResolve(specifier);
- // Check against the fully resolved URL, not just the specifier, in case another loader has
- // something to contribute to the resolution.
- const ext = path.extname(stripExtras(/** @type {FileURL} */ (nextResult.url)));
-
- parentURLs.set(
- /** @type {FileURL} */ (nextResult.url),
- /** @type {FileURL} */ (ctx.parentURL ?? pathToFileURL(path.join(cwd(), 'whatever.ext')).href),
- );
-
- if (ext === '.jsx') {
- return {
- ...nextResult,
- format: 'jsx',
- };
- }
-
- if (ext === '.mts' || ext === '.ts' || ext === '.tsx') {
- return {
- ...nextResult,
- format: 'tsx',
- };
- }
-
- return nextResult;
-}
-export { resolveTSX as resolve };
-
-/**
- * @type {import('node:module').LoadHook}
- * @param {FileURL} url The fully resolved url.
- */
-async function loadTSX(url, ctx, nextLoad) {
- if (ctx.format !== 'jsx' && ctx.format !== 'tsx') return nextLoad(url); // not (j|t)sx
-
- const format = 'module';
- const esbuildConfig = {
- jsx: 'automatic',
- jsxFactory: "h",
- /*Corresponds to tsconfig jsxFragmentFactory*/
- jsxFragment: "Fragment",
- jsxImportSource: "preact",
- jsxDev: true,
- loader: 'tsx',
- minify: true,
- sourcemap: true,
- };
-
- const nextResult = await nextLoad(url, {
- format,
- });
- let rawSource = `${nextResult.source}`; // byte array → string
-
- const { code: source, warnings } = await transform(rawSource, {
- sourcefile: url,
- ...esbuildConfig,
- }).catch(({ errors }) => {
- for (const {
- location: { column, line, lineText },
- text,
- } of errors) {
- // oxlint-disable-next-line no-console
- console.error(
- `TranspileError: ${text}\n at ${url}:${line}:${column}\n at: ${lineText}\n`,
- );
- }
-
- return {
- code: null,
- warnings: [],
- };
- });
-
- // oxlint-disable-next-line no-console
- if (warnings?.length) console.warn(...warnings);
-
- return {
- format,
- source,
- };
-}
-export { loadTSX as load };
-
-if (isMainThread && 'register' in module) {
- module.register('./tsx-loader.js', import.meta.url);
-}