+14
-13
src/hooks/useFileUpload.ts
+14
-13
src/hooks/useFileUpload.ts
···
1
-
import { parseFile, PlatformParseError } from '../lib/platforms/parser';
2
-
import type { SocialUser } from '../lib/platforms/types';
1
+
import { parseDataFile } from '../lib/fileExtractor';
3
2
import type { SearchResult } from '../types';
4
3
5
4
export function useFileUpload(
···
11
10
if (!file) return;
12
11
13
12
onStatusUpdate(`Processing ${file.name}...`);
14
-
let users: SocialUser[] = [];
13
+
let usernames: string[] = [];
15
14
16
15
try {
17
-
// Use the new platform-based parser
18
-
users = await parseFile(file, platform);
16
+
usernames = await parseDataFile(file, platform);
19
17
20
-
console.log(`Loaded ${users.length} users from ${platform} data`);
21
-
onStatusUpdate(`Loaded ${users.length} users from ${platform} data`);
18
+
console.log(`Loaded ${usernames.length} users from ${platform} data`);
19
+
onStatusUpdate(`Loaded ${usernames.length} users from ${platform} data`);
22
20
} catch (error) {
23
21
console.error("Error processing file:", error);
24
22
25
-
const errorMsg = error instanceof PlatformParseError
23
+
const errorMsg = error instanceof Error
26
24
? error.message
27
25
: "There was a problem processing the file. Please check that it's a valid data export.";
28
26
···
31
29
return;
32
30
}
33
31
34
-
if (users.length === 0) {
32
+
if (usernames.length === 0) {
35
33
const errorMsg = "No users found in the file.";
36
34
onStatusUpdate(errorMsg);
37
35
alert(errorMsg);
38
36
return;
39
37
}
40
38
41
-
// Initialize search results
42
-
const initialResults: SearchResult[] = users.map(user => ({
43
-
sourceUser: user,
39
+
// Initialize search results - convert usernames to SearchResult format
40
+
const initialResults: SearchResult[] = usernames.map(username => ({
41
+
sourceUser: {
42
+
username: username,
43
+
date: ''
44
+
},
44
45
atprotoMatches: [],
45
46
isSearching: false,
46
47
selectedMatches: new Set<string>(),
47
48
sourcePlatform: platform
48
49
}));
49
50
50
-
onStatusUpdate(`Starting search for ${users.length} users...`);
51
+
onStatusUpdate(`Starting search for ${usernames.length} users...`);
51
52
onSearchStart(initialResults, platform);
52
53
}
53
54
+123
src/lib/fileExtractor.ts
+123
src/lib/fileExtractor.ts
···
1
+
import JSZip from 'jszip';
2
+
import { ParseRule, getRulesForPlatform, FileFormat } from './platformDefinitions';
3
+
import { parseContent } from './parserLogic';
4
+
5
+
// Type for the final aggregated results
6
+
export interface ExtractionResults {
7
+
allExtracted: Record<string, string[]>;
8
+
uniqueUsernames: string[];
9
+
}
10
+
11
+
export class DataExtractor {
12
+
private file: File | ArrayBuffer | Blob;
13
+
14
+
constructor(file: File | ArrayBuffer | Blob) {
15
+
this.file = file;
16
+
}
17
+
18
+
public async processZipArchive(zip: JSZip, rules: ParseRule[]): Promise<ExtractionResults> {
19
+
/** Core logic for extracting usernames from a successfully loaded ZIP archive. */
20
+
const allExtracted: Record<string, string[]> = {};
21
+
const uniqueUsernames: Set<string> = new Set();
22
+
23
+
for (let i = 0; i < rules.length; i++) {
24
+
const rule = rules[i];
25
+
const ruleId = `Rule_${i + 1}_${rule.zipPath}`;
26
+
console.log(`Processing ZIP file path ${rule.zipPath} (Format: ${rule.format})`);
27
+
28
+
// 1. Get file object from ZIP
29
+
const fileInZip = zip.file(rule.zipPath);
30
+
if (!fileInZip) {
31
+
console.warn(`WARNING: File not found in ZIP: '${rule.zipPath}'. Skipping rule.`);
32
+
continue;
33
+
}
34
+
35
+
try {
36
+
// 2. Read content asynchronously
37
+
const content = await fileInZip.async("string");
38
+
39
+
// 3. Apply appropriate parsing logic
40
+
const extracted = parseContent(content, rule);
41
+
42
+
// 4. Store results
43
+
allExtracted[ruleId] = extracted;
44
+
extracted.forEach(name => uniqueUsernames.add(name));
45
+
46
+
} catch (e) {
47
+
console.error(`ERROR reading file ${rule.zipPath} from ZIP:`, e);
48
+
}
49
+
}
50
+
51
+
return {
52
+
allExtracted,
53
+
uniqueUsernames: Array.from(uniqueUsernames).sort()
54
+
};
55
+
}
56
+
}
57
+
58
+
/**
59
+
* Public facing function handling both ZIP and single files.
60
+
* @param file A File object (or ArrayBuffer/Blob) representing the uploaded data.
61
+
* @param platform The platform name (e.g., 'instagram', 'tiktok').
62
+
* @returns A promise that resolves to an array of unique usernames (string[]).
63
+
*/
64
+
export async function parseDataFile(file: File | ArrayBuffer | Blob, platform: string): Promise<string[]> {
65
+
const rules = getRulesForPlatform(platform);
66
+
67
+
if (rules.length === 0) {
68
+
console.error(`No parsing rules found for platform: ${platform}`);
69
+
return [];
70
+
}
71
+
72
+
// 1. --- ATTEMPT ZIP LOAD ---
73
+
try {
74
+
console.log("Attempting to load file as ZIP archive...");
75
+
const zip = await JSZip.loadAsync(file);
76
+
77
+
const extractor = new DataExtractor(file);
78
+
const results = await extractor.processZipArchive(zip, rules);
79
+
80
+
console.log(`Successfully extracted ${results.uniqueUsernames.length} usernames from ZIP archive.`);
81
+
return results.uniqueUsernames;
82
+
83
+
} catch (e) {
84
+
// 2. --- ZIP LOAD FAILED, ATTEMPT SINGLE FILE ---
85
+
console.warn("ZIP load failed. Attempting to parse file as a single data file...");
86
+
87
+
// We need a File object to get the name and content easily
88
+
if (!(file instanceof File) && !(file instanceof Blob)) {
89
+
console.error("Input failed ZIP check and lacks a name/content structure for single file parsing (must be File or Blob).");
90
+
return [];
91
+
}
92
+
93
+
const singleFile = file as File;
94
+
95
+
// Find the rule that matches the uploaded file name
96
+
// We check if the uploaded filename ends with the final part of a rule's zipPath (e.g., "following.html")
97
+
const matchingRule = rules.find(rule =>
98
+
singleFile.name.toLowerCase().endsWith((rule.zipPath.split('/').pop() || '').toLowerCase())
99
+
);
100
+
101
+
if (!matchingRule) {
102
+
console.error(`Could not match single file '${singleFile.name}' to any rule for platform ${platform}. Check rules in platformDefinitions.ts.`);
103
+
return [];
104
+
}
105
+
106
+
console.log(`Matched single file '${singleFile.name}' to rule: ${matchingRule.zipPath}`);
107
+
108
+
// 3. Process as single file content
109
+
try {
110
+
const content = await singleFile.text();
111
+
const extracted = parseContent(content, matchingRule);
112
+
113
+
const uniqueUsernames = Array.from(new Set(extracted)).sort();
114
+
console.log(`Successfully extracted ${uniqueUsernames.length} unique usernames from single file.`);
115
+
116
+
return uniqueUsernames;
117
+
118
+
} catch (contentError) {
119
+
console.error("Error reading content of single file:", contentError);
120
+
return [];
121
+
}
122
+
}
123
+
}
+108
src/lib/parserLogic.ts
+108
src/lib/parserLogic.ts
···
1
+
import { ParseRule, FileFormat } from './platformDefinitions';
2
+
3
+
/**
4
+
* Parses content using a regular expression.
5
+
* @param content The string content (HTML or plain text) to search within.
6
+
* @param regexPattern The regex string defining the capture group for the username.
7
+
* @returns An array of extracted usernames.
8
+
*/
9
+
export function parseTextOrHtml(content: string, regexPattern: string): string[] {
10
+
try {
11
+
// 'g' for global matching, 's' for multiline (DOTALL equivalent)
12
+
const pattern = new RegExp(regexPattern, 'gs');
13
+
14
+
// matchAll returns an iterator of matches; we spread it into an array.
15
+
const matches = [...content.matchAll(pattern)];
16
+
17
+
// We map the results to the first captured group (match[1]), filtering out empty results.
18
+
return matches.map(match => match[1]).filter(name => !!name);
19
+
20
+
} catch (e) {
21
+
console.error(`ERROR: Invalid regex pattern '${regexPattern}':`, e);
22
+
return [];
23
+
}
24
+
}
25
+
26
+
/**
27
+
* Traverses a JSON object structure to extract usernames from a nested array of objects.
28
+
* Assumes the common pattern: navigate to an array, and extract a key from each object in that array.
29
+
* @param content The JSON content as a string.
30
+
* @param pathKeys The array of keys defining the path, where the second to last key is the array key, and the last key is the target username field.
31
+
* @returns An array of extracted usernames.
32
+
*/
33
+
export function parseJson(content: string, pathKeys: string[]): string[] {
34
+
try {
35
+
const data = JSON.parse(content);
36
+
const usernames: string[] = [];
37
+
38
+
if (pathKeys.length < 2) {
39
+
console.error("JSON rule must have at least two path keys (list key and target key).");
40
+
return [];
41
+
}
42
+
43
+
// Determine the navigation path
44
+
let currentData: any = data;
45
+
const listContainerPath = pathKeys.slice(0, -2);
46
+
const listKey = pathKeys[pathKeys.length - 2];
47
+
const targetKey = pathKeys[pathKeys.length - 1];
48
+
49
+
// 1. Traverse down to the object containing the target array
50
+
for (const key of listContainerPath) {
51
+
if (typeof currentData === 'object' && currentData !== null && key in currentData) {
52
+
currentData = currentData[key];
53
+
} else {
54
+
console.error(`ERROR: Could not traverse JSON path up to key: ${key}. Path: ${listContainerPath.join(' -> ')}`);
55
+
return [];
56
+
}
57
+
}
58
+
59
+
// 2. Check if the penultimate key holds the array
60
+
if (typeof currentData === 'object' && currentData !== null && listKey in currentData) {
61
+
const userList = currentData[listKey];
62
+
63
+
if (Array.isArray(userList)) {
64
+
// 3. Iterate over the array and extract the final target key
65
+
for (const item of userList) {
66
+
if (typeof item === 'object' && item !== null && targetKey in item) {
67
+
// Found the username
68
+
usernames.push(String(item[targetKey]));
69
+
}
70
+
}
71
+
} else {
72
+
console.error(`ERROR: Expected an array at key '${listKey}' but found a different type.`);
73
+
}
74
+
} else {
75
+
console.error(`ERROR: List key '${listKey}' not found at its expected position.`);
76
+
}
77
+
78
+
return usernames;
79
+
80
+
} catch (e) {
81
+
if (e instanceof SyntaxError) {
82
+
console.error(`ERROR: Could not decode JSON content:`, e);
83
+
} else {
84
+
console.error(`An unexpected error occurred during JSON parsing:`, e);
85
+
}
86
+
return [];
87
+
}
88
+
}
89
+
90
+
/**
91
+
* Universal wrapper to apply the correct parsing method based on the rule's format.
92
+
* @param content The file content as a string.
93
+
* @param rule The ParseRule to apply.
94
+
* @returns An array of extracted usernames.
95
+
*/
96
+
export function parseContent(content: string, rule: ParseRule): string[] {
97
+
if (rule.format === 'HTML' || rule.format === 'TEXT') {
98
+
if (typeof rule.rule === 'string') {
99
+
return parseTextOrHtml(content, rule.rule);
100
+
}
101
+
} else if (rule.format === 'JSON') {
102
+
if (Array.isArray(rule.rule)) {
103
+
return parseJson(content, rule.rule);
104
+
}
105
+
}
106
+
console.error(`ERROR: Unsupported format or invalid rule type for rule with path: ${rule.zipPath}`);
107
+
return [];
108
+
}
+53
src/lib/platformDefinitions.ts
+53
src/lib/platformDefinitions.ts
···
1
+
// Use string literals for type safety on formats
2
+
export type FileFormat = 'HTML' | 'TEXT' | 'JSON';
3
+
4
+
// Define the structure for a single parsing rule
5
+
export interface ParseRule {
6
+
zipPath: string; // File path *inside* the ZIP archive
7
+
format: FileFormat; // Expected format of the file, e.g. 'HTML', 'TEXT', 'JSON'
8
+
rule: string | string[]; // specific extraction rule (regex pattern string or JSON key path array)
9
+
}
10
+
11
+
/*
12
+
PLATFORM DEFINITIONS
13
+
This constant holds all the defined extraction rules, grouped by platform.
14
+
*/
15
+
16
+
export const PLATFORM_RULES: Record<string, ParseRule[]> = {
17
+
18
+
"instagram": [
19
+
{
20
+
zipPath: "connections/followers_and_following/following.html",
21
+
format: "HTML",
22
+
// Regex captures the username group 'beautyscicomm' from the URL:
23
+
// https://www.instagram.com/_u/beautyscicomm
24
+
// Note: The 'g' and 's' flags are handled in the extractor method.
25
+
rule: '<a target="_blank" href="https://www.instagram.com/_u/([^"]+)"'
26
+
},
27
+
{
28
+
zipPath: "connections/followers_and_following/following.json",
29
+
format: "JSON",
30
+
rule: ["relationships_following", "title"]
31
+
}
32
+
],
33
+
34
+
"tiktok": [
35
+
{
36
+
zipPath: "TikTok/Profile and Settings/Following.txt",
37
+
format: "TEXT",
38
+
// Regex captures the text after "Username: " on the same line
39
+
rule: "Username:\s*([^\r\n]+)"
40
+
},
41
+
{
42
+
zipPath: "tiktok.json",
43
+
format: "JSON",
44
+
// JSON key path to traverse: ['Your Activity'] -> ['Following'] -> ['Following'] -> 'UserName'
45
+
rule: ["Your Activity", "Following", "Following", "UserName"]
46
+
}
47
+
],
48
+
};
49
+
50
+
export function getRulesForPlatform(platformName: string): ParseRule[] {
51
+
// Retrieves the list of parsing rules for a given platform.
52
+
return PLATFORM_RULES[platformName.toLowerCase()] || [];
53
+
}
-149
src/lib/platforms/instagram.ts
-149
src/lib/platforms/instagram.ts
···
1
-
// src/lib/platforms/instagram.ts
2
-
3
-
import type { PlatformConfig, PlatformParser, FileBundle, SocialUser } from './types';
4
-
import { PlatformParseError } from './types';
5
-
6
-
// HTML Parser for Instagram following.html
7
-
const htmlParser: PlatformParser = {
8
-
name: 'Instagram HTML',
9
-
canParse: (bundle: FileBundle) => {
10
-
for (const [_, file] of bundle.files) {
11
-
if (file.type === 'html' && file.name.toLowerCase().includes('following')) {
12
-
return file.content.includes('_a6-g') || file.content.includes('uiBoxWhite');
13
-
}
14
-
}
15
-
return false;
16
-
},
17
-
parse: async (bundle: FileBundle) => {
18
-
const users: SocialUser[] = [];
19
-
20
-
// Find HTML file
21
-
let htmlContent = '';
22
-
for (const [_, file] of bundle.files) {
23
-
if (file.type === 'html' && file.name.toLowerCase().includes('following')) {
24
-
htmlContent = file.content;
25
-
break;
26
-
}
27
-
}
28
-
29
-
if (!htmlContent) {
30
-
throw new PlatformParseError('No Instagram following.html file found', 'instagram');
31
-
}
32
-
33
-
// Parse the HTML
34
-
const parser = new DOMParser();
35
-
const doc = parser.parseFromString(htmlContent, 'text/html');
36
-
37
-
// Instagram following data is in specific divs
38
-
const userDivs = doc.querySelectorAll('div.pam._3-95._2ph-._a6-g.uiBoxWhite.noborder');
39
-
40
-
userDivs.forEach((div) => {
41
-
const h2 = div.querySelector('h2._3-95._2pim._a6-h._a6-i');
42
-
const dateDiv = div.querySelector('div._a6-p > div > div:nth-child(2)');
43
-
44
-
if (h2) {
45
-
const username = h2.textContent?.trim();
46
-
const date = dateDiv?.textContent?.trim() || '';
47
-
48
-
if (username) {
49
-
users.push({
50
-
username: username,
51
-
date: date
52
-
});
53
-
}
54
-
}
55
-
});
56
-
57
-
if (users.length === 0) {
58
-
throw new PlatformParseError(
59
-
'No following data found in Instagram HTML file',
60
-
'instagram'
61
-
);
62
-
}
63
-
64
-
return users;
65
-
}
66
-
};
67
-
68
-
// JSON Parser for Instagram JSON exports
69
-
const jsonParser: PlatformParser = {
70
-
name: 'Instagram JSON',
71
-
canParse: (bundle: FileBundle) => {
72
-
for (const [_, file] of bundle.files) {
73
-
if (file.type === 'json') {
74
-
try {
75
-
const data = JSON.parse(file.content);
76
-
return !!(data?.relationships_following || data?.following);
77
-
} catch {
78
-
return false;
79
-
}
80
-
}
81
-
}
82
-
return false;
83
-
},
84
-
parse: async (bundle: FileBundle) => {
85
-
const users: SocialUser[] = [];
86
-
87
-
// Find and parse JSON file
88
-
for (const [_, file] of bundle.files) {
89
-
if (file.type === 'json') {
90
-
try {
91
-
const jsonData = JSON.parse(file.content);
92
-
93
-
// Instagram JSON exports can have different structures
94
-
let followingArray = jsonData?.relationships_following;
95
-
96
-
if (!followingArray && jsonData?.following) {
97
-
followingArray = jsonData.following;
98
-
}
99
-
100
-
if (!Array.isArray(followingArray)) {
101
-
continue;
102
-
}
103
-
104
-
for (const entry of followingArray) {
105
-
const username = entry.string_list_data?.[0]?.value || entry.username || entry.handle;
106
-
const timestamp = entry.string_list_data?.[0]?.timestamp || entry.timestamp;
107
-
108
-
if (username) {
109
-
users.push({
110
-
username: username,
111
-
date: timestamp ? new Date(timestamp * 1000).toISOString() : ''
112
-
});
113
-
}
114
-
}
115
-
116
-
if (users.length > 0) {
117
-
return users;
118
-
}
119
-
} catch (e) {
120
-
continue;
121
-
}
122
-
}
123
-
}
124
-
125
-
throw new PlatformParseError(
126
-
'No valid Instagram JSON data found. Expected relationships_following or following array',
127
-
'instagram'
128
-
);
129
-
}
130
-
};
131
-
132
-
// Instagram Platform Configuration
133
-
export const instagramPlatform: PlatformConfig = {
134
-
id: 'instagram',
135
-
name: 'Instagram',
136
-
parsers: [htmlParser, jsonParser], // Try HTML first (most common)
137
-
expectedFiles: ['following.html', 'connections.json', 'followers_and_following.json'],
138
-
validate: (bundle: FileBundle) => {
139
-
// Check if bundle contains Instagram-like files
140
-
for (const [path, file] of bundle.files) {
141
-
if (path.toLowerCase().includes('instagram') ||
142
-
path.toLowerCase().includes('connections') ||
143
-
(file.name.toLowerCase().includes('following') && file.type === 'html')) {
144
-
return true;
145
-
}
146
-
}
147
-
return false;
148
-
}
149
-
};
-120
src/lib/platforms/parser.ts
-120
src/lib/platforms/parser.ts
···
1
-
import JSZip from "jszip";
2
-
import type { FileBundle, SocialUser } from './types';
3
-
import { PlatformParseError } from './types';
4
-
import { getPlatform } from './registry';
5
-
6
-
// Convert a file into a FileBundle (extract ZIP if needed)
7
-
async function createBundle(file: File): Promise<FileBundle> {
8
-
const bundle: FileBundle = {
9
-
files: new Map(),
10
-
originalFileName: file.name
11
-
};
12
-
13
-
if (file.name.endsWith('.zip')) {
14
-
// Extract ZIP contents
15
-
const zip = await JSZip.loadAsync(file);
16
-
17
-
for (const [path, zipEntry] of Object.entries(zip.files)) {
18
-
if (zipEntry.dir) continue; // Skip directories
19
-
20
-
const content = await zipEntry.async('string');
21
-
const fileName = path.split('/').pop() || path;
22
-
23
-
// Determine file type
24
-
let type: 'text' | 'html' | 'json' = 'text';
25
-
if (fileName.endsWith('.html')) type = 'html';
26
-
else if (fileName.endsWith('.json')) type = 'json';
27
-
else if (fileName.endsWith('.txt')) type = 'text';
28
-
29
-
bundle.files.set(path, {
30
-
name: fileName,
31
-
content,
32
-
type
33
-
});
34
-
}
35
-
} else {
36
-
// Single file
37
-
const content = await file.text();
38
-
let type: 'text' | 'html' | 'json' = 'text';
39
-
40
-
if (file.name.endsWith('.html')) type = 'html';
41
-
else if (file.name.endsWith('.json')) type = 'json';
42
-
else if (file.name.endsWith('.txt')) type = 'text';
43
-
44
-
bundle.files.set(file.name, {
45
-
name: file.name,
46
-
content,
47
-
type
48
-
});
49
-
}
50
-
51
-
return bundle;
52
-
}
53
-
54
-
/**
55
-
* Parse a file for a specific platform
56
-
*/
57
-
export async function parseFile(file: File, platformId: string): Promise<SocialUser[]> {
58
-
// Get platform config
59
-
const platform = getPlatform(platformId);
60
-
if (!platform) {
61
-
throw new PlatformParseError(
62
-
`Platform '${platformId}' is not supported`,
63
-
platformId
64
-
);
65
-
}
66
-
67
-
// Create file bundle
68
-
const bundle = await createBundle(file);
69
-
70
-
if (bundle.files.size === 0) {
71
-
throw new PlatformParseError(
72
-
'No files found in upload',
73
-
platformId
74
-
);
75
-
}
76
-
77
-
// Validate bundle contains expected files (optional check)
78
-
if (!platform.validate(bundle)) {
79
-
const expectedFiles = platform.expectedFiles.join(', ');
80
-
throw new PlatformParseError(
81
-
`File doesn't appear to be ${platform.name} data. Expected files like: ${expectedFiles}`,
82
-
platformId
83
-
);
84
-
}
85
-
86
-
// Try each parser in order
87
-
const errors: string[] = [];
88
-
89
-
for (const parser of platform.parsers) {
90
-
if (!parser.canParse(bundle)) {
91
-
continue; // Skip parsers that can't handle this bundle
92
-
}
93
-
94
-
try {
95
-
const users = await parser.parse(bundle);
96
-
97
-
if (users.length === 0) {
98
-
errors.push(`${parser.name}: No users found`);
99
-
continue;
100
-
}
101
-
102
-
console.log(`Successfully parsed ${users.length} users using ${parser.name}`);
103
-
return users;
104
-
} catch (error) {
105
-
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
106
-
errors.push(`${parser.name}: ${errorMsg}`);
107
-
console.warn(`${parser.name} failed:`, errorMsg);
108
-
}
109
-
}
110
-
111
-
// All parsers failed
112
-
throw new PlatformParseError(
113
-
`Could not parse ${platform.name} data. Tried: ${errors.join('; ')}`,
114
-
platformId
115
-
);
116
-
}
117
-
118
-
// Export for backwards compatibility
119
-
export { PlatformParseError } from './types';
120
-
export type { SocialUser } from './types';
-26
src/lib/platforms/registry.ts
-26
src/lib/platforms/registry.ts
···
1
-
import type { PlatformConfig } from './types';
2
-
import { tiktokPlatform } from './tiktok';
3
-
import { instagramPlatform } from './instagram';
4
-
5
-
// Registry of all supported platforms
6
-
const platformRegistry = new Map<string, PlatformConfig>();
7
-
8
-
// Register platforms
9
-
platformRegistry.set('tiktok', tiktokPlatform);
10
-
platformRegistry.set('instagram', instagramPlatform);
11
-
12
-
// Future platforms can be added here:
13
-
// platformRegistry.set('twitter', twitterPlatform);
14
-
// platformRegistry.set('youtube', youtubePlatform);
15
-
16
-
export function getPlatform(platformId: string): PlatformConfig | undefined {
17
-
return platformRegistry.get(platformId);
18
-
}
19
-
20
-
export function getAllPlatforms(): PlatformConfig[] {
21
-
return Array.from(platformRegistry.values());
22
-
}
23
-
24
-
export function isPlatformSupported(platformId: string): boolean {
25
-
return platformRegistry.has(platformId);
26
-
}
-114
src/lib/platforms/tiktok.ts
-114
src/lib/platforms/tiktok.ts
···
1
-
import type { PlatformConfig, PlatformParser, FileBundle, SocialUser } from './types';
2
-
import { PlatformParseError } from './types';
3
-
4
-
// TXT Parser for TikTok Following.txt format
5
-
const txtParser: PlatformParser = {
6
-
name: 'TikTok TXT',
7
-
canParse: (bundle: FileBundle) => {
8
-
// Look for .txt files that might be TikTok format
9
-
for (const [_, file] of bundle.files) {
10
-
if (file.name.toLowerCase().includes('following') && file.type === 'text') {
11
-
return file.content.includes('Username:');
12
-
}
13
-
}
14
-
return false;
15
-
},
16
-
parse: async (bundle: FileBundle) => {
17
-
const users: SocialUser[] = [];
18
-
19
-
// Find the TikTok following.txt file
20
-
let content = '';
21
-
for (const [_, file] of bundle.files) {
22
-
if (file.name.toLowerCase().includes('following') && file.type === 'text') {
23
-
content = file.content;
24
-
break;
25
-
}
26
-
}
27
-
28
-
if (!content) {
29
-
throw new PlatformParseError('No TikTok following.txt file found', 'tiktok');
30
-
}
31
-
32
-
const entries = content.split("\n\n").map((b) => b.trim()).filter(Boolean);
33
-
34
-
for (const entry of entries) {
35
-
const userMatch = entry.match(/Username:\s*(.+)/);
36
-
if (userMatch) {
37
-
users.push({ username: userMatch[1].trim(), date: "" });
38
-
}
39
-
}
40
-
41
-
return users;
42
-
}
43
-
};
44
-
45
-
// JSON Parser for TikTok JSON exports
46
-
const jsonParser: PlatformParser = {
47
-
name: 'TikTok JSON',
48
-
canParse: (bundle: FileBundle) => {
49
-
for (const [_, file] of bundle.files) {
50
-
if (file.type === 'json') {
51
-
try {
52
-
const data = JSON.parse(file.content);
53
-
return !!data?.["Your Activity"]?.["Following"]?.["Following"];
54
-
} catch {
55
-
return false;
56
-
}
57
-
}
58
-
}
59
-
return false;
60
-
},
61
-
parse: async (bundle: FileBundle) => {
62
-
const users: SocialUser[] = [];
63
-
64
-
// Find and parse JSON file
65
-
for (const [_, file] of bundle.files) {
66
-
if (file.type === 'json') {
67
-
try {
68
-
const jsonData = JSON.parse(file.content);
69
-
const followingArray = jsonData?.["Your Activity"]?.["Following"]?.["Following"];
70
-
71
-
if (!followingArray || !Array.isArray(followingArray)) {
72
-
continue;
73
-
}
74
-
75
-
for (const entry of followingArray) {
76
-
users.push({
77
-
username: entry.UserName,
78
-
date: entry.Date || "",
79
-
});
80
-
}
81
-
82
-
if (users.length > 0) {
83
-
return users;
84
-
}
85
-
} catch (e) {
86
-
continue;
87
-
}
88
-
}
89
-
}
90
-
91
-
throw new PlatformParseError(
92
-
'No valid TikTok JSON data found. Expected path: Your Activity > Following > Following',
93
-
'tiktok'
94
-
);
95
-
}
96
-
};
97
-
98
-
// TikTok Platform Configuration
99
-
export const tiktokPlatform: PlatformConfig = {
100
-
id: 'tiktok',
101
-
name: 'TikTok',
102
-
parsers: [txtParser, jsonParser], // Try TXT first (most common)
103
-
expectedFiles: ['Following.txt', 'user_data.json'],
104
-
validate: (bundle: FileBundle) => {
105
-
// Check if bundle contains TikTok-like files
106
-
for (const [path, file] of bundle.files) {
107
-
if (path.toLowerCase().includes('tiktok') ||
108
-
(file.name.toLowerCase().includes('following') && file.type === 'text')) {
109
-
return true;
110
-
}
111
-
}
112
-
return false;
113
-
}
114
-
};
-30
src/lib/platforms/types.ts
-30
src/lib/platforms/types.ts
···
1
-
export interface SocialUser {
2
-
username: string;
3
-
date: string;
4
-
}
5
-
6
-
export interface FileBundle {
7
-
files: Map<string, { name: string; content: string; type: 'text' | 'html' | 'json' }>;
8
-
originalFileName: string;
9
-
}
10
-
11
-
export interface PlatformParser {
12
-
name: string;
13
-
parse: (bundle: FileBundle) => Promise<SocialUser[]>;
14
-
canParse: (bundle: FileBundle) => boolean;
15
-
}
16
-
17
-
export interface PlatformConfig {
18
-
id: string;
19
-
name: string;
20
-
parsers: PlatformParser[];
21
-
expectedFiles: string[]; // File patterns to look for
22
-
validate: (bundle: FileBundle) => boolean;
23
-
}
24
-
25
-
export class PlatformParseError extends Error {
26
-
constructor(message: string, public platform: string) {
27
-
super(message);
28
-
this.name = 'PlatformParseError';
29
-
}
30
-
}