initial commit

This commit is contained in:
moro
2025-10-17 00:14:13 +07:00
commit 7bc1dfcc3b
41 changed files with 12890 additions and 0 deletions

38
.gitignore vendored Normal file
View File

@@ -0,0 +1,38 @@
# dependencies (bun install)
node_modules
# output
out
dist
*.tgz
# code coverage
coverage
*.lcov
# logs
logs
_.log
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# caches
.eslintcache
.cache
*.tsbuildinfo
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store
htmlexample
results

0
README.md Normal file
View File

47
Taskfile.yml Normal file
View File

@@ -0,0 +1,47 @@
version: "3"
tasks:
build:
desc: Build TypeScript files in src/ to dist/ using Bun
cmds:
- bun build ./src/popup/popup.ts --outdir ./dist/popup
- bun build ./src/background.ts --outdir ./dist
- bun build ./src/contentScript.ts --outdir ./dist
- cp ./src/popup/popup.html ./dist/popup/popup.html
- cp ./src/popup/popup.css ./dist/popup/popup.css
- cp ./src/public/manifest.json ./dist/manifest.json
- cp ./src/mark.css ./dist/mark.css
- zip -r ./public/extension.zip ./dist
dev:
desc: Watch mode development
cmds:
- task: clean
- task: build
- |
bun --watch build --outdir ./dist/popup ./src/popup/popup.ts &
bun --watch build --outdir ./dist ./src/background.ts &
bun --watch build --outdir ./dist ./src/contentScript.ts &
while inotifywait -e modify,create,delete -r ./src/popup/popup.html ./src/popup/popup.css ./src/public/manifest.json .src/; do
cp ./src/popup/popup.html ./dist/popup/popup.html
cp ./src/popup/popup.css ./dist/popup/popup.css
cp ./src/mark.css ./dist/mark.css
cp ./src/public/manifest.json ./dist/manifest.json
done
wait
clean:
desc: Remove dist directory
cmds:
- rm -rf dist
lint:
desc: Run TypeScript compiler for type checking
cmds:
- tsc --noEmit
start:
desc: Alias for dev
cmds:
- task: dev

288
bun.lock Normal file
View File

@@ -0,0 +1,288 @@
{
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "new-browser-input",
"dependencies": {
"@types/turndown": "^5.0.5",
"turndown": "^7.2.0",
"turndown-plugin-gfm": "^1.0.2",
},
"devDependencies": {
"@types/bun": "latest",
"@types/chrome": "^0.0.326",
"@typescript-eslint/eslint-plugin": "^8.34.0",
"@typescript-eslint/parser": "^8.34.0",
"eslint": "^9.28.0",
},
"peerDependencies": {
"typescript": "^5.8.3",
},
},
},
"packages": {
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.7.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw=="],
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="],
"@eslint/config-array": ["@eslint/config-array@0.20.0", "", { "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ=="],
"@eslint/config-helpers": ["@eslint/config-helpers@0.2.2", "", {}, "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg=="],
"@eslint/core": ["@eslint/core@0.14.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg=="],
"@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="],
"@eslint/js": ["@eslint/js@9.28.0", "", {}, "sha512-fnqSjGWd/CoIp4EXIxWVK/sHA6DOHN4+8Ix2cX5ycOY7LG0UY8nHCU5pIp2eaE1Mc7Qd8kHspYNzYXT2ojPLzg=="],
"@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="],
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.3.1", "", { "dependencies": { "@eslint/core": "^0.14.0", "levn": "^0.4.1" } }, "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w=="],
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
"@humanfs/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="],
"@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
"@mixmark-io/domino": ["@mixmark-io/domino@2.2.0", "", {}, "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw=="],
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
"@types/bun": ["@types/bun@1.2.15", "", { "dependencies": { "bun-types": "1.2.15" } }, "sha512-U1ljPdBEphF0nw1MIk0hI7kPg7dFdPyM7EenHsp6W5loNHl7zqy6JQf/RKCgnUn2KDzUpkBwHPnEJEjII594bA=="],
"@types/chrome": ["@types/chrome@0.0.326", "", { "dependencies": { "@types/filesystem": "*", "@types/har-format": "*" } }, "sha512-WS7jKf3ZRZFHOX7dATCZwqNJgdfiSF0qBRFxaO0LhIOvTNBrfkab26bsZwp6EBpYtqp8loMHJTnD6vDTLWPKYw=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/filesystem": ["@types/filesystem@0.0.36", "", { "dependencies": { "@types/filewriter": "*" } }, "sha512-vPDXOZuannb9FZdxgHnqSwAG/jvdGM8Wq+6N4D/d80z+D4HWH+bItqsZaVRQykAn6WEVeEkLm2oQigyHtgb0RA=="],
"@types/filewriter": ["@types/filewriter@0.0.33", "", {}, "sha512-xFU8ZXTw4gd358lb2jw25nxY9QAgqn2+bKKjKOYfNCzN4DKCFetK7sPtrlpg66Ywe3vWY9FNxprZawAh9wfJ3g=="],
"@types/har-format": ["@types/har-format@1.2.16", "", {}, "sha512-fluxdy7ryD3MV6h8pTfTYpy/xQzCFC7m89nOH9y94cNqJ1mDIDPut7MnRHI3F6qRmh/cT2fUjG1MLdCNb4hE9A=="],
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
"@types/node": ["@types/node@24.0.0", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-yZQa2zm87aRVcqDyH5+4Hv9KYgSdgwX1rFnGvpbzMaC7YAljmhBET93TPiTd3ObwTL+gSpIzPKg5BqVxdCvxKg=="],
"@types/turndown": ["@types/turndown@5.0.5", "", {}, "sha512-TL2IgGgc7B5j78rIccBtlYAnkuv8nUQqhQc+DSYV5j9Be9XOcm/SKOVRuA47xAVI3680Tk9B1d8flK2GWT2+4w=="],
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.34.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.34.0", "@typescript-eslint/type-utils": "8.34.0", "@typescript-eslint/utils": "8.34.0", "@typescript-eslint/visitor-keys": "8.34.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.34.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-QXwAlHlbcAwNlEEMKQS2RCgJsgXrTJdjXT08xEgbPFa2yYQgVjBymxP5DrfrE7X7iodSzd9qBUHUycdyVJTW1w=="],
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.34.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.34.0", "@typescript-eslint/types": "8.34.0", "@typescript-eslint/typescript-estree": "8.34.0", "@typescript-eslint/visitor-keys": "8.34.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-vxXJV1hVFx3IXz/oy2sICsJukaBrtDEQSBiV48/YIV5KWjX1dO+bcIr/kCPrW6weKXvsaGKFNlwH0v2eYdRRbA=="],
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.34.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.34.0", "@typescript-eslint/types": "^8.34.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-iEgDALRf970/B2YExmtPMPF54NenZUf4xpL3wsCRx/lgjz6ul/l13R81ozP/ZNuXfnLCS+oPmG7JIxfdNYKELw=="],
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.34.0", "", { "dependencies": { "@typescript-eslint/types": "8.34.0", "@typescript-eslint/visitor-keys": "8.34.0" } }, "sha512-9Ac0X8WiLykl0aj1oYQNcLZjHgBojT6cW68yAgZ19letYu+Hxd0rE0veI1XznSSst1X5lwnxhPbVdwjDRIomRw=="],
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.34.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-+W9VYHKFIzA5cBeooqQxqNriAP0QeQ7xTiDuIOr71hzgffm3EL2hxwWBIIj4GuofIbKxGNarpKqIq6Q6YrShOA=="],
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.34.0", "", { "dependencies": { "@typescript-eslint/typescript-estree": "8.34.0", "@typescript-eslint/utils": "8.34.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-n7zSmOcUVhcRYC75W2pnPpbO1iwhJY3NLoHEtbJwJSNlVAZuwqu05zY3f3s2SDWWDSo9FdN5szqc73DCtDObAg=="],
"@typescript-eslint/types": ["@typescript-eslint/types@8.34.0", "", {}, "sha512-9V24k/paICYPniajHfJ4cuAWETnt7Ssy+R0Rbcqo5sSFr3QEZ/8TSoUi9XeXVBGXCaLtwTOKSLGcInCAvyZeMA=="],
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.34.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.34.0", "@typescript-eslint/tsconfig-utils": "8.34.0", "@typescript-eslint/types": "8.34.0", "@typescript-eslint/visitor-keys": "8.34.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-rOi4KZxI7E0+BMqG7emPSK1bB4RICCpF7QD3KCLXn9ZvWoESsOMlHyZPAHyG04ujVplPaHbmEvs34m+wjgtVtg=="],
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.34.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.34.0", "@typescript-eslint/types": "8.34.0", "@typescript-eslint/typescript-estree": "8.34.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-8L4tWatGchV9A1cKbjaavS6mwYwp39jql8xUmIIKJdm+qiaeHy5KMKlBrf30akXAWBzn2SqKsNOtSENWUwg7XQ=="],
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.34.0", "", { "dependencies": { "@typescript-eslint/types": "8.34.0", "eslint-visitor-keys": "^4.2.0" } }, "sha512-qHV7pW7E85A0x6qyrFn+O+q1k1p3tQCsqIZ1KZ5ESLXY57aTvUd3/a4rdPTeXisvhXn2VQG0VSKUqs8KHF2zcA=="],
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="],
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
"bun-types": ["bun-types@1.2.15", "", { "dependencies": { "@types/node": "*" } }, "sha512-NarRIaS+iOaQU1JPfyKhZm4AsUOrwUOqRNHY0XxI8GI8jYxiLXLcdjYMG9UKS+fwWasc1uw1htV9AX24dD+p4w=="],
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
"debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
"eslint": ["eslint@9.28.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.20.0", "@eslint/config-helpers": "^0.2.1", "@eslint/core": "^0.14.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.28.0", "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.3.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-ocgh41VhRlf9+fVpe7QKzwLj9c92fDiqOj8Y3Sd4/ZmVA4Btx4PlUYPq4pp9JDyupkf1upbEXecxL2mwNV7jPQ=="],
"eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
"eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
"espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="],
"esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="],
"esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
"estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
"fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="],
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
"find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
"flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
"flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="],
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
"globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
"graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="],
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
"ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
"minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
"picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
"semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
"ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="],
"turndown": ["turndown@7.2.0", "", { "dependencies": { "@mixmark-io/domino": "^2.2.0" } }, "sha512-eCZGBN4nNNqM9Owkv9HAtWRYfLA4h909E/WGAWWBpmB275ehNhZyk87/Tpvjbp0jjNl9XwCsbe6bm6CqFsgD+A=="],
"turndown-plugin-gfm": ["turndown-plugin-gfm@1.0.2", "", {}, "sha512-vwz9tfvF7XN/jE0dGoBei3FXWuvll78ohzCZQuOb+ZjWrs3a0XhQVomJEb2Qh4VHTPNRO4GPZh0V7VRbiWwkRg=="],
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
"undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
"@eslint/eslintrc/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
"@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="],
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"eslint/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
}
}

View File

@@ -0,0 +1,766 @@
[
{
"facts": [
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/",
"name": "url",
"type": "url",
"value": "https://www.instagram.com/p/DMVZrA6hg4s/"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/",
"name": "username",
"type": "string",
"value": "jokowi"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/",
"name": "content",
"type": "string",
"value": "Terima kasih Bapak Presiden Prabowo yang telah berkenan berkunjung kerumah keluarga kami di Solo"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/",
"name": "likeCount",
"type": "number",
"value": "480461"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/",
"name": "commentCount",
"type": "number",
"value": "13247"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/",
"name": "createdAt",
"type": "datetime",
"value": "1753024829"
}
],
"savedAt": "2025-07-22T13:00:16.478Z",
"type": "post",
"url": "https://www.instagram.com/p/DMVZrA6hg4s/"
},
{
"facts": [
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=jeus.simorangkir",
"name": "username",
"type": "string",
"value": "jeus.simorangkir"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=jeus.simorangkir",
"name": "content",
"type": "string",
"value": "TOMBOL HIDUP PAK JOKOWI DAN SEHAT SELALU🔥🔥🔥"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=jeus.simorangkir",
"name": "profileUrl",
"type": "url",
"value": "https://www.instagram.com/jeus.simorangkir/"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=jeus.simorangkir",
"name": "profileImage",
"type": "url",
"value": "https://instagram.fdps17-1.fna.fbcdn.net/v/t51.2885-19/322979687_211880454702196_544204852800779256_n.jpg?stp=dst-jpg_s150x150_tt6&efg=eyJ2ZW5jb2RlX3RhZyI6InByb2ZpbGVfcGljLmRqYW5nby44MjguYzIifQ&_nc_ht=instagram.fdps17-1.fna.fbcdn.net&_nc_cat=104&_nc_oc=Q6cZ2QEaTl0keKoiKKc6csP2iVA4GuXZLcz8QtSg9_FYgwP0G4r2rVJmXH2pJfYTzpTRXeI&_nc_ohc=GqDz8hC__CIQ7kNvwF9-w44&_nc_gid=_-U-WBOzzjboJzKoYa-Reg&edm=APs17CUBAAAA&ccb=7-5&oh=00_AfT_j_J85vGg7Dh5NbYez0nQ6jgaKggFeqrTGvnqOUpgdA&oe=68854C2E&_nc_sid=10d13b"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=jeus.simorangkir",
"name": "timestamp",
"type": "datetime",
"value": "2025-07-20T15:32:08.000Z"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=jeus.simorangkir",
"name": "timestampDisplay",
"type": "string",
"value": "1d"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=jeus.simorangkir",
"name": "likes",
"type": "number",
"value": "7457"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=jeus.simorangkir",
"name": "replies",
"type": "number",
"value": "86"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=angga.sudibyo",
"name": "username",
"type": "string",
"value": "angga.sudibyo"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=angga.sudibyo",
"name": "content",
"type": "string",
"value": "Presiden terbaik dan mantan presiden terbaik, sehat2 selalu pak Jokowi dan pak Prabowo..❤️"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=angga.sudibyo",
"name": "profileUrl",
"type": "url",
"value": "https://www.instagram.com/angga.sudibyo/"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=angga.sudibyo",
"name": "profileImage",
"type": "url",
"value": "https://instagram.fdps17-1.fna.fbcdn.net/v/t51.2885-19/130992335_2899596323615403_6005254264143156705_n.jpg?stp=dst-jpg_s150x150_tt6&efg=eyJ2ZW5jb2RlX3RhZyI6InByb2ZpbGVfcGljLmRqYW5nby43MjAuYzIifQ&_nc_ht=instagram.fdps17-1.fna.fbcdn.net&_nc_cat=103&_nc_oc=Q6cZ2QEaTl0keKoiKKc6csP2iVA4GuXZLcz8QtSg9_FYgwP0G4r2rVJmXH2pJfYTzpTRXeI&_nc_ohc=98ypeD2jh0UQ7kNvwG3K6qe&_nc_gid=_-U-WBOzzjboJzKoYa-Reg&edm=APs17CUBAAAA&ccb=7-5&oh=00_AfSWbG3p05bu31rxEAX4E7rrj4sG3EvPzDIBmqx8gBeGfg&oe=68857447&_nc_sid=10d13b"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=angga.sudibyo",
"name": "timestamp",
"type": "datetime",
"value": "2025-07-20T15:21:50.000Z"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=angga.sudibyo",
"name": "timestampDisplay",
"type": "string",
"value": "1d"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=angga.sudibyo",
"name": "likes",
"type": "number",
"value": "6374"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=angga.sudibyo",
"name": "replies",
"type": "number",
"value": "138"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=hubbimuhammad",
"name": "username",
"type": "string",
"value": "hubbimuhammad"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=hubbimuhammad",
"name": "content",
"type": "string",
"value": "Wiwok detok"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=hubbimuhammad",
"name": "profileUrl",
"type": "url",
"value": "https://www.instagram.com/hubbimuhammad/"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=hubbimuhammad",
"name": "profileImage",
"type": "url",
"value": "https://instagram.fdps17-1.fna.fbcdn.net/v/t51.2885-19/236438774_1003962147037394_5773809719213231100_n.jpg?stp=dst-jpg_s150x150_tt6&efg=eyJ2ZW5jb2RlX3RhZyI6InByb2ZpbGVfcGljLmRqYW5nby4xMDgwLmMyIn0&_nc_ht=instagram.fdps17-1.fna.fbcdn.net&_nc_cat=102&_nc_oc=Q6cZ2QEaTl0keKoiKKc6csP2iVA4GuXZLcz8QtSg9_FYgwP0G4r2rVJmXH2pJfYTzpTRXeI&_nc_ohc=Uj3-r0VSatoQ7kNvwFmHm57&_nc_gid=_-U-WBOzzjboJzKoYa-Reg&edm=APs17CUBAAAA&ccb=7-5&oh=00_AfTe1e2-KCVDwT9pylBSDKZPk5Cbblq8XHWSovIYuRTB3Q&oe=688554AB&_nc_sid=10d13b"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=hubbimuhammad",
"name": "timestamp",
"type": "datetime",
"value": "2025-07-20T15:46:10.000Z"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=hubbimuhammad",
"name": "timestampDisplay",
"type": "string",
"value": "1d"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=hubbimuhammad",
"name": "likes",
"type": "number",
"value": "201"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=hubbimuhammad",
"name": "replies",
"type": "number",
"value": "6"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=dr_amar___Verified",
"name": "username",
"type": "string",
"value": "dr_amar___Verified"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=dr_amar___Verified",
"name": "content",
"type": "string",
"value": "dr_amar___"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=dr_amar___Verified",
"name": "profileUrl",
"type": "url",
"value": "https://www.instagram.com/dr_amar___/"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=dr_amar___Verified",
"name": "profileImage",
"type": "url",
"value": "https://instagram.fdps17-1.fna.fbcdn.net/v/t51.2885-19/371372306_352964293817124_2624783820092888996_n.jpg?stp=dst-jpg_s150x150_tt6&efg=eyJ2ZW5jb2RlX3RhZyI6InByb2ZpbGVfcGljLmRqYW5nby4xMDc5LmMyIn0&_nc_ht=instagram.fdps17-1.fna.fbcdn.net&_nc_cat=100&_nc_oc=Q6cZ2QEaTl0keKoiKKc6csP2iVA4GuXZLcz8QtSg9_FYgwP0G4r2rVJmXH2pJfYTzpTRXeI&_nc_ohc=E8WNw887aPEQ7kNvwFFJyCm&_nc_gid=_-U-WBOzzjboJzKoYa-Reg&edm=APs17CUBAAAA&ccb=7-5&oh=00_AfSfxrjfovS9WBPQon5Z3LX6n-3FwCzM-lxuKY-keNE60Q&oe=68856DAF&_nc_sid=10d13b"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=dr_amar___Verified",
"name": "timestamp",
"type": "datetime",
"value": "2025-07-20T15:30:13.000Z"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=dr_amar___Verified",
"name": "timestampDisplay",
"type": "string",
"value": "1d"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=dr_amar___Verified",
"name": "likes",
"type": "number",
"value": "3850"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=dr_amar___Verified",
"name": "replies",
"type": "number",
"value": "27"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=0_stanly_0",
"name": "username",
"type": "string",
"value": "0_stanly_0"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=0_stanly_0",
"name": "content",
"type": "string",
"value": "Indah nya kebersamaan para pemimpin bangsa"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=0_stanly_0",
"name": "profileUrl",
"type": "url",
"value": "https://www.instagram.com/0_stanly_0/"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=0_stanly_0",
"name": "profileImage",
"type": "url",
"value": "https://instagram.fdps17-1.fna.fbcdn.net/v/t51.2885-19/321843554_1273204200194583_2783570366240423599_n.jpg?stp=dst-jpg_s150x150_tt6&efg=eyJ2ZW5jb2RlX3RhZyI6InByb2ZpbGVfcGljLmRqYW5nby4xMDgwLmMyIn0&_nc_ht=instagram.fdps17-1.fna.fbcdn.net&_nc_cat=103&_nc_oc=Q6cZ2QEaTl0keKoiKKc6csP2iVA4GuXZLcz8QtSg9_FYgwP0G4r2rVJmXH2pJfYTzpTRXeI&_nc_ohc=oBdLq4jqMKYQ7kNvwGT8ws8&_nc_gid=_-U-WBOzzjboJzKoYa-Reg&edm=APs17CUBAAAA&ccb=7-5&oh=00_AfRRh7dEs0eJuwaJx_wLnzxTsSg3Qo988djd9ehPS_SLqQ&oe=68854E46&_nc_sid=10d13b"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=0_stanly_0",
"name": "timestamp",
"type": "datetime",
"value": "2025-07-20T15:32:17.000Z"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=0_stanly_0",
"name": "timestampDisplay",
"type": "string",
"value": "1d"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=0_stanly_0",
"name": "likes",
"type": "number",
"value": "1587"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=0_stanly_0",
"name": "replies",
"type": "number",
"value": "17"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=riansya_14",
"name": "username",
"type": "string",
"value": "riansya_14"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=riansya_14",
"name": "content",
"type": "string",
"value": "Full penjilat😂"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=riansya_14",
"name": "profileUrl",
"type": "url",
"value": "https://www.instagram.com/riansya_14/"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=riansya_14",
"name": "profileImage",
"type": "url",
"value": "https://instagram.fdps17-1.fna.fbcdn.net/v/t51.2885-19/481788343_1696217614321706_2181028944150197706_n.jpg?stp=dst-jpg_s150x150_tt6&efg=eyJ2ZW5jb2RlX3RhZyI6InByb2ZpbGVfcGljLmRqYW5nby4xMDgwLmMyIn0&_nc_ht=instagram.fdps17-1.fna.fbcdn.net&_nc_cat=102&_nc_oc=Q6cZ2QEaTl0keKoiKKc6csP2iVA4GuXZLcz8QtSg9_FYgwP0G4r2rVJmXH2pJfYTzpTRXeI&_nc_ohc=a10J0WzO0pwQ7kNvwFBpxu6&_nc_gid=_-U-WBOzzjboJzKoYa-Reg&edm=APs17CUBAAAA&ccb=7-5&oh=00_AfTYsWzU58ByhjeDbSCnu_r_9PgcKiBuAmoDV4-ne6qalQ&oe=688568FB&_nc_sid=10d13b"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=riansya_14",
"name": "timestamp",
"type": "datetime",
"value": "2025-07-20T22:36:53.000Z"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=riansya_14",
"name": "timestampDisplay",
"type": "string",
"value": "1d"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=riansya_14",
"name": "likes",
"type": "number",
"value": "251"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=riansya_14",
"name": "replies",
"type": "number",
"value": "8"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=cipris_1303",
"name": "username",
"type": "string",
"value": "cipris_1303"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=cipris_1303",
"name": "content",
"type": "string",
"value": "Presiden terbaik dan mantan presiden terbaik 🇮🇩sehat selalu pak dan penuh berkat"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=cipris_1303",
"name": "profileUrl",
"type": "url",
"value": "https://www.instagram.com/cipris_1303/"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=cipris_1303",
"name": "profileImage",
"type": "url",
"value": "https://instagram.fdps17-1.fna.fbcdn.net/v/t51.2885-19/142858735_422560428973422_381148379720372387_n.jpg?stp=dst-jpg_s150x150_tt6&efg=eyJ2ZW5jb2RlX3RhZyI6InByb2ZpbGVfcGljLmRqYW5nby4xMDgwLmMyIn0&_nc_ht=instagram.fdps17-1.fna.fbcdn.net&_nc_cat=106&_nc_oc=Q6cZ2QEaTl0keKoiKKc6csP2iVA4GuXZLcz8QtSg9_FYgwP0G4r2rVJmXH2pJfYTzpTRXeI&_nc_ohc=s86wIuGVFJgQ7kNvwG0sf68&_nc_gid=_-U-WBOzzjboJzKoYa-Reg&edm=APs17CUBAAAA&ccb=7-5&oh=00_AfTe6WX77ADhOfPa3zr6FuhRt4QuvoSLXK01Wxcj4edIvA&oe=6885429A&_nc_sid=10d13b"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=cipris_1303",
"name": "timestamp",
"type": "datetime",
"value": "2025-07-20T23:28:05.000Z"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=cipris_1303",
"name": "timestampDisplay",
"type": "string",
"value": "1d"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=cipris_1303",
"name": "likes",
"type": "number",
"value": "1506"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=cipris_1303",
"name": "replies",
"type": "number",
"value": "19"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=prayogaza1",
"name": "username",
"type": "string",
"value": "prayogaza1"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=prayogaza1",
"name": "content",
"type": "string",
"value": "Merusak Tatanan negara"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=prayogaza1",
"name": "profileUrl",
"type": "url",
"value": "https://www.instagram.com/prayogaza1/"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=prayogaza1",
"name": "profileImage",
"type": "url",
"value": "https://instagram.fdps17-1.fna.fbcdn.net/v/t51.2885-19/515761503_18081358546871883_7627397364361180093_n.jpg?stp=dst-jpg_s150x150_tt6&efg=eyJ2ZW5jb2RlX3RhZyI6InByb2ZpbGVfcGljLmRqYW5nby4xMDgwLmMyIn0&_nc_ht=instagram.fdps17-1.fna.fbcdn.net&_nc_cat=103&_nc_oc=Q6cZ2QEaTl0keKoiKKc6csP2iVA4GuXZLcz8QtSg9_FYgwP0G4r2rVJmXH2pJfYTzpTRXeI&_nc_ohc=irohdz2KZJEQ7kNvwEZpRIF&_nc_gid=_-U-WBOzzjboJzKoYa-Reg&edm=APs17CUBAAAA&ccb=7-5&oh=00_AfTm_Qsry34RyzquINRic-v-bSIuKf8VJMEM2qhfagVWzw&oe=688546BC&_nc_sid=10d13b"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=prayogaza1",
"name": "timestamp",
"type": "datetime",
"value": "2025-07-21T00:56:42.000Z"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=prayogaza1",
"name": "timestampDisplay",
"type": "string",
"value": "1d"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=prayogaza1",
"name": "likes",
"type": "number",
"value": "565"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=prayogaza1",
"name": "replies",
"type": "number",
"value": "14"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=togarsitumorangofficialVerified",
"name": "username",
"type": "string",
"value": "togarsitumorangofficialVerified"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=togarsitumorangofficialVerified",
"name": "content",
"type": "string",
"value": "togarsitumorangofficial"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=togarsitumorangofficialVerified",
"name": "profileUrl",
"type": "url",
"value": "https://www.instagram.com/togarsitumorangofficial/"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=togarsitumorangofficialVerified",
"name": "profileImage",
"type": "url",
"value": "https://instagram.fdps17-1.fna.fbcdn.net/v/t51.2885-19/402739086_1059202125232608_3579411792619616185_n.jpg?stp=dst-jpg_s150x150_tt6&efg=eyJ2ZW5jb2RlX3RhZyI6InByb2ZpbGVfcGljLmRqYW5nby4xMDgwLmMyIn0&_nc_ht=instagram.fdps17-1.fna.fbcdn.net&_nc_cat=100&_nc_oc=Q6cZ2QEaTl0keKoiKKc6csP2iVA4GuXZLcz8QtSg9_FYgwP0G4r2rVJmXH2pJfYTzpTRXeI&_nc_ohc=HYiQ5qjvW4EQ7kNvwHRMkUe&_nc_gid=_-U-WBOzzjboJzKoYa-Reg&edm=APs17CUBAAAA&ccb=7-5&oh=00_AfT5oO3RDah0Srg61xAT-LB5p3mndqL_ti1Iv4vsEFyV7A&oe=6885559F&_nc_sid=10d13b"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=togarsitumorangofficialVerified",
"name": "timestamp",
"type": "datetime",
"value": "2025-07-20T23:11:43.000Z"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=togarsitumorangofficialVerified",
"name": "timestampDisplay",
"type": "string",
"value": "1d"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=togarsitumorangofficialVerified",
"name": "likes",
"type": "number",
"value": "2959"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=togarsitumorangofficialVerified",
"name": "replies",
"type": "number",
"value": "86"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=28_rynt",
"name": "username",
"type": "string",
"value": "28_rynt"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=28_rynt",
"name": "content",
"type": "string",
"value": "banyak buzzer nya ya ternyata 😂"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=28_rynt",
"name": "profileUrl",
"type": "url",
"value": "https://www.instagram.com/28_rynt/"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=28_rynt",
"name": "profileImage",
"type": "url",
"value": "https://instagram.fdps17-1.fna.fbcdn.net/v/t51.2885-19/518501742_17939081997031581_6677039797516417206_n.jpg?stp=dst-jpg_s150x150_tt6&efg=eyJ2ZW5jb2RlX3RhZyI6InByb2ZpbGVfcGljLmRqYW5nby4xMDgwLmMyIn0&_nc_ht=instagram.fdps17-1.fna.fbcdn.net&_nc_cat=109&_nc_oc=Q6cZ2QEaTl0keKoiKKc6csP2iVA4GuXZLcz8QtSg9_FYgwP0G4r2rVJmXH2pJfYTzpTRXeI&_nc_ohc=wSTQ-uSfplkQ7kNvwH1I5Wg&_nc_gid=_-U-WBOzzjboJzKoYa-Reg&edm=APs17CUBAAAA&ccb=7-5&oh=00_AfS1lyPhB-WlI5qjJ8tn1KgqjpjG7ObK7Ws9HGtvF4nUlw&oe=68856E37&_nc_sid=10d13b"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=28_rynt",
"name": "timestamp",
"type": "datetime",
"value": "2025-07-20T15:37:45.000Z"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=28_rynt",
"name": "timestampDisplay",
"type": "string",
"value": "1d"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=28_rynt",
"name": "likes",
"type": "number",
"value": "729"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=28_rynt",
"name": "replies",
"type": "number",
"value": "26"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=luxvillle_",
"name": "username",
"type": "string",
"value": "luxvillle_"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=luxvillle_",
"name": "content",
"type": "string",
"value": "ijazah mana wi"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=luxvillle_",
"name": "profileUrl",
"type": "url",
"value": "https://www.instagram.com/luxvillle_/"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=luxvillle_",
"name": "profileImage",
"type": "url",
"value": "https://instagram.fcpq1-1.fna.fbcdn.net/v/t51.2885-19/464760996_1254146839119862_3605321457742435801_n.png?stp=dst-jpg_e0_s150x150_tt6&cb=8577c754-c2464923&efg=eyJ2ZW5jb2RlX3RhZyI6InByb2ZpbGVfcGljLmRqYW5nby4xNTAuYzIifQ&_nc_ht=instagram.fcpq1-1.fna.fbcdn.net&_nc_cat=1&_nc_oc=Q6cZ2QEaG6Jc7yy7TBh3OXus9ZPqiTE7msO3-ppRfRbIu3XYolX60KH8ILHIlfhHK3GQ1OnFv5A20Yos2h-2EiD8Pa6B&_nc_ohc=wwy9HdRJcYoQ7kNvwFOLNAK&_nc_gid=WjEoo0wXG3yPxgmIBxxJCw&edm=AB11_MABAAAA&ccb=7-5&ig_cache_key=YW5vbnltb3VzX3Byb2ZpbGVfcGlj.3-ccb7-5-cb8577c754-c2464923&oh=00_AfQbu6nENHFskdPLtIGGTEEjmuDgY-MTOzx28TFmSGks7w&oe=68855F28&_nc_sid=dc5e7f"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=luxvillle_",
"name": "timestamp",
"type": "datetime",
"value": "2025-07-20T15:55:10.000Z"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=luxvillle_",
"name": "timestampDisplay",
"type": "string",
"value": "1d"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=luxvillle_",
"name": "likes",
"type": "number",
"value": "141"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=luxvillle_",
"name": "replies",
"type": "number",
"value": "4"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=srikandi_sarangheyo",
"name": "username",
"type": "string",
"value": "srikandi_sarangheyo"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=srikandi_sarangheyo",
"name": "content",
"type": "string",
"value": "bpk jokowi ttp dihati rakyatnya,,masyaallah sht sll bpk smoga allah mlindungi bpk bserta kluarga,,bpk adlh president terbaik yg prnh bangsa indonesia miliki🔥🔥"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=srikandi_sarangheyo",
"name": "profileUrl",
"type": "url",
"value": "https://www.instagram.com/srikandi_sarangheyo/"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=srikandi_sarangheyo",
"name": "profileImage",
"type": "url",
"value": "https://instagram.fdps17-1.fna.fbcdn.net/v/t51.2885-19/511638400_18008389931784271_6744633162976478269_n.jpg?stp=dst-jpg_s150x150_tt6&efg=eyJ2ZW5jb2RlX3RhZyI6InByb2ZpbGVfcGljLmRqYW5nby4xMDgwLmMyIn0&_nc_ht=instagram.fdps17-1.fna.fbcdn.net&_nc_cat=107&_nc_oc=Q6cZ2QEaTl0keKoiKKc6csP2iVA4GuXZLcz8QtSg9_FYgwP0G4r2rVJmXH2pJfYTzpTRXeI&_nc_ohc=uv_u2hKQs0EQ7kNvwEdYLR7&_nc_gid=_-U-WBOzzjboJzKoYa-Reg&edm=APs17CUBAAAA&ccb=7-5&oh=00_AfS8u2iUSeHLBq-BGzAAnvIawjWLOR_epP0b-TCYAEyUCw&oe=6885504A&_nc_sid=10d13b"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=srikandi_sarangheyo",
"name": "timestamp",
"type": "datetime",
"value": "2025-07-20T15:25:59.000Z"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=srikandi_sarangheyo",
"name": "timestampDisplay",
"type": "string",
"value": "1d"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=srikandi_sarangheyo",
"name": "likes",
"type": "number",
"value": "1589"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=srikandi_sarangheyo",
"name": "replies",
"type": "number",
"value": "34"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=aufa_suryanata",
"name": "username",
"type": "string",
"value": "aufa_suryanata"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=aufa_suryanata",
"name": "content",
"type": "string",
"value": "Anjay banyak bner buzer 58🔥"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=aufa_suryanata",
"name": "profileUrl",
"type": "url",
"value": "https://www.instagram.com/aufa_suryanata/"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=aufa_suryanata",
"name": "profileImage",
"type": "url",
"value": "https://instagram.fcpq1-1.fna.fbcdn.net/v/t51.2885-19/464760996_1254146839119862_3605321457742435801_n.png?stp=dst-jpg_e0_s150x150_tt6&cb=8577c754-c2464923&efg=eyJ2ZW5jb2RlX3RhZyI6InByb2ZpbGVfcGljLmRqYW5nby4xNTAuYzIifQ&_nc_ht=instagram.fcpq1-1.fna.fbcdn.net&_nc_cat=1&_nc_oc=Q6cZ2QEaG6Jc7yy7TBh3OXus9ZPqiTE7msO3-ppRfRbIu3XYolX60KH8ILHIlfhHK3GQ1OnFv5A20Yos2h-2EiD8Pa6B&_nc_ohc=wwy9HdRJcYoQ7kNvwFOLNAK&_nc_gid=WjEoo0wXG3yPxgmIBxxJCw&edm=AB11_MABAAAA&ccb=7-5&ig_cache_key=YW5vbnltb3VzX3Byb2ZpbGVfcGlj.3-ccb7-5-cb8577c754-c2464923&oh=00_AfQbu6nENHFskdPLtIGGTEEjmuDgY-MTOzx28TFmSGks7w&oe=68855F28&_nc_sid=dc5e7f"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=aufa_suryanata",
"name": "timestamp",
"type": "datetime",
"value": "2025-07-22T07:19:31.000Z"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=aufa_suryanata",
"name": "timestampDisplay",
"type": "string",
"value": "5h"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=aufa_suryanata",
"name": "likes",
"type": "number",
"value": "20"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=mhmmdalfarhzy",
"name": "username",
"type": "string",
"value": "mhmmdalfarhzy"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=mhmmdalfarhzy",
"name": "content",
"type": "string",
"value": "Neraka pedih loh pak"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=mhmmdalfarhzy",
"name": "profileUrl",
"type": "url",
"value": "https://www.instagram.com/mhmmdalfarhzy/"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=mhmmdalfarhzy",
"name": "profileImage",
"type": "url",
"value": "https://instagram.fdps17-1.fna.fbcdn.net/v/t51.2885-19/469247047_2288541594840403_8660919058584834110_n.jpg?stp=dst-jpg_s150x150_tt6&efg=eyJ2ZW5jb2RlX3RhZyI6InByb2ZpbGVfcGljLmRqYW5nby4xMDgwLmMyIn0&_nc_ht=instagram.fdps17-1.fna.fbcdn.net&_nc_cat=107&_nc_oc=Q6cZ2QEaTl0keKoiKKc6csP2iVA4GuXZLcz8QtSg9_FYgwP0G4r2rVJmXH2pJfYTzpTRXeI&_nc_ohc=hLDSRNi7BUIQ7kNvwFRdWyf&_nc_gid=_-U-WBOzzjboJzKoYa-Reg&edm=APs17CUBAAAA&ccb=7-5&oh=00_AfTOj2NHnotkdZC8GdpMEiCOrqXIL9H7XfmKnXkfdALcxQ&oe=688561D1&_nc_sid=10d13b"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=mhmmdalfarhzy",
"name": "timestamp",
"type": "datetime",
"value": "2025-07-21T05:49:38.000Z"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=mhmmdalfarhzy",
"name": "timestampDisplay",
"type": "string",
"value": "1d"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=mhmmdalfarhzy",
"name": "likes",
"type": "number",
"value": "93"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=mhmmdalfarhzy",
"name": "replies",
"type": "number",
"value": "1"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=nicke_cubby",
"name": "username",
"type": "string",
"value": "nicke_cubby"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=nicke_cubby",
"name": "content",
"type": "string",
"value": "Ma Sya Allah 💕 adem liat 2 idola rakyat Indonesia ini 😍❤️"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=nicke_cubby",
"name": "profileUrl",
"type": "url",
"value": "https://www.instagram.com/nicke_cubby/"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=nicke_cubby",
"name": "profileImage",
"type": "url",
"value": "https://instagram.fdps17-1.fna.fbcdn.net/v/t51.2885-19/109421841_2712419008993610_1068649358446491562_n.jpg?stp=dst-jpg_s150x150_tt6&efg=eyJ2ZW5jb2RlX3RhZyI6InByb2ZpbGVfcGljLmRqYW5nby4xMDgwLmMyIn0&_nc_ht=instagram.fdps17-1.fna.fbcdn.net&_nc_cat=102&_nc_oc=Q6cZ2QEaTl0keKoiKKc6csP2iVA4GuXZLcz8QtSg9_FYgwP0G4r2rVJmXH2pJfYTzpTRXeI&_nc_ohc=wvTdnd3HqdcQ7kNvwHkdBNY&_nc_gid=_-U-WBOzzjboJzKoYa-Reg&edm=APs17CUBAAAA&ccb=7-5&oh=00_AfRmc1NYxyktG2CjJar9oaULcg6tYkN7QcWF1lKcEsOgUw&oe=68855D4B&_nc_sid=10d13b"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=nicke_cubby",
"name": "timestamp",
"type": "datetime",
"value": "2025-07-20T15:33:03.000Z"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=nicke_cubby",
"name": "timestampDisplay",
"type": "string",
"value": "1d"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=nicke_cubby",
"name": "likes",
"type": "number",
"value": "1138"
},
{
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=nicke_cubby",
"name": "replies",
"type": "number",
"value": "19"
}
],
"savedAt": "2025-07-22T13:00:16.481Z",
"type": "comment",
"url": "https://www.instagram.com/p/DMVZrA6hg4s/"
}
]

View File

@@ -0,0 +1,262 @@
[
{
"facts": [
{
"context": "https://www.instagram.com/jokowi/",
"name": "username",
"type": "string",
"value": "jokowi"
},
{
"context": "https://www.instagram.com/jokowi/",
"name": "profileImage",
"type": "url",
"value": "https://instagram.fdps17-1.fna.fbcdn.net/v/t51.2885-19/323796872_186095804235435_4099239256456033831_n.jpg?stp=dst-jpg_s150x150_tt6&efg=eyJ2ZW5jb2RlX3RhZyI6InByb2ZpbGVfcGljLmRqYW5nby4xMDgwLmMyIn0&_nc_ht=instagram.fdps17-1.fna.fbcdn.net&_nc_cat=1&_nc_oc=Q6cZ2QFFdz0HZSILtfF6Rzwy-7QTXpo6wKoqXnj-acYERdRgEiC2b09IA6YcYhLsoIcwtnM&_nc_ohc=FBLkTWMPdcEQ7kNvwEsWlAd&_nc_gid=wq3phUaaZcxz05d9N8p1CQ&edm=AP4sbd4BAAAA&ccb=7-5&oh=00_AfQLAy2aidNZT4Wwk2yjo-vzoQnYGy3TGkcncOMrfS0Fig&oe=68856664&_nc_sid=7a9f4b"
},
{
"context": "https://www.instagram.com/jokowi/",
"name": "isVerified",
"type": "boolean",
"value": "true"
},
{
"context": "https://www.instagram.com/jokowi/",
"name": "postCount",
"type": "number",
"value": "5775"
},
{
"context": "https://www.instagram.com/jokowi/",
"name": "followingCount",
"type": "number",
"value": "0"
}
],
"savedAt": "2025-07-22T12:58:32.657Z",
"type": "profile",
"url": "https://www.instagram.com/jokowi/"
},
{
"facts": [
{
"context": "https://www.instagram.com/jokowi/reel/DMYCVUvB9nM/",
"name": "url",
"type": "url",
"value": "https://www.instagram.com/jokowi/reel/DMYCVUvB9nM/"
},
{
"context": "https://www.instagram.com/jokowi/reel/DMYCVUvB9nM/",
"name": "thumbnailUrl",
"type": "url",
"value": "https://scontent.cdninstagram.com/v/t51.71878-15/521411526_1921944571903368_794586515625105760_n.jpg?stp=dst-jpg_e15_tt6&_nc_cat=1&ig_cache_key=MzY4MTcwMjk1Njg0MTI5NDI4NA%3D%3D.3-ccb1-7&ccb=1-7&_nc_sid=58cdad&efg=eyJ2ZW5jb2RlX3RhZyI6InhwaWRzLjY0MHgxMTM2LnNkciJ9&_nc_ohc=2zdzHEyojxUQ7kNvwEPv5y-&_nc_oc=AdkldIrtVzgWwYEOaLiluo1rvyS_A2-B693SDTzUAO3OYgrQU5x1xPjBZG_R3ImK3eE&_nc_ad=z-m&_nc_cid=0&_nc_zt=23&_nc_ht=scontent.cdninstagram.com&_nc_gid=wq3phUaaZcxz05d9N8p1CQ&oh=00_AfRIXv_U69iPTc24p6k-cAozoWWtH2OkttS6sBK8AdyvUw&oe=688559A1"
},
{
"context": "https://www.instagram.com/jokowi/reel/DMYCVUvB9nM/",
"name": "content",
"type": "string",
"value": "Saya melepas kepulangan Bapak Presiden Prabowo di Bandara Adi Soemarmo. Semoga lancar dan aman sampai tujuan."
},
{
"context": "https://www.instagram.com/jokowi/reel/DMXGi5vhtc0/",
"name": "url",
"type": "url",
"value": "https://www.instagram.com/jokowi/reel/DMXGi5vhtc0/"
},
{
"context": "https://www.instagram.com/jokowi/reel/DMXGi5vhtc0/",
"name": "thumbnailUrl",
"type": "url",
"value": "https://scontent.cdninstagram.com/v/t51.71878-15/522690099_2768611616656149_5103259914330463227_n.jpg?stp=dst-jpg_e15_tt6&_nc_cat=1&ig_cache_key=MzY4MTQ0MDAwNzE0MDU5NTUwOA%3D%3D.3-ccb1-7&ccb=1-7&_nc_sid=58cdad&efg=eyJ2ZW5jb2RlX3RhZyI6InhwaWRzLjY0MHgxMTM2LnNkciJ9&_nc_ohc=C7PTKqmsmd0Q7kNvwE3rKzd&_nc_oc=AdnzWqP91dz_O-galyznJsFEEVa-gde1YhxXUIjyv0ksUGdWV1WE-2C3fUZT9kHESEA&_nc_ad=z-m&_nc_cid=0&_nc_zt=23&_nc_ht=scontent.cdninstagram.com&_nc_gid=wq3phUaaZcxz05d9N8p1CQ&oh=00_AfT-kRhFgWph3lL0bQl7yD9bIjADvynfJcWHZLN0JP7PGw&oe=6885662A"
},
{
"context": "https://www.instagram.com/jokowi/reel/DMXGi5vhtc0/",
"name": "content",
"type": "string",
"value": "Saya mengajak Bapak Presiden Prabowo menikmati hidangan Bakmi Bu Citro di Solo. Rumah makan sederhana yang punya rasa istimewa. Kami duduk santai, berbincang ringan ditemani semangkuk bakmi hangat. Sebuah momen personal, jauh dari agenda kenegaraan, dan berkesan. Terima kasih Pak Presiden"
},
{
"context": "https://www.instagram.com/jokowi/reel/DMVZrA6hg4s/",
"name": "url",
"type": "url",
"value": "https://www.instagram.com/jokowi/reel/DMVZrA6hg4s/"
},
{
"context": "https://www.instagram.com/jokowi/reel/DMVZrA6hg4s/",
"name": "thumbnailUrl",
"type": "url",
"value": "https://scontent.cdninstagram.com/v/t51.71878-15/521542555_1681705549156478_6214360678065370356_n.jpg?stp=dst-jpg_e15_tt6&_nc_cat=1&ig_cache_key=MzY4MDk2MTE3NzUyNzM4OTc0MA%3D%3D.3-ccb1-7&ccb=1-7&_nc_sid=58cdad&efg=eyJ2ZW5jb2RlX3RhZyI6InhwaWRzLjY0MHgxMTM2LnNkciJ9&_nc_ohc=cZRzNEaAtoMQ7kNvwEwQnCJ&_nc_oc=AdngYOxEGuzX9uDXG93nZ4gJHlfFPstMyFFdtBQ6InHETh-psATyJflNlMaPqxh2cfI&_nc_ad=z-m&_nc_cid=0&_nc_zt=23&_nc_ht=scontent.cdninstagram.com&_nc_gid=wq3phUaaZcxz05d9N8p1CQ&oh=00_AfRXFZaASm9kMqbECDOXllWQryvG0UEe3_OIrsahtVIlNg&oe=68855335"
},
{
"context": "https://www.instagram.com/jokowi/reel/DMVZrA6hg4s/",
"name": "content",
"type": "string",
"value": "Terima kasih Bapak Presiden Prabowo yang telah berkenan berkunjung kerumah keluarga kami di Solo"
},
{
"context": "https://www.instagram.com/jokowi/reel/DMFubsQhxmM/",
"name": "url",
"type": "url",
"value": "https://www.instagram.com/jokowi/reel/DMFubsQhxmM/"
},
{
"context": "https://www.instagram.com/jokowi/reel/DMFubsQhxmM/",
"name": "thumbnailUrl",
"type": "url",
"value": "https://scontent.cdninstagram.com/v/t51.71878-15/519411790_1237579021145938_1676132418911526372_n.jpg?stp=dst-jpg_e15_tt6&_nc_cat=100&ig_cache_key=MzY3NjU0ODg4MzkwNTE5MDI4NA%3D%3D.3-ccb1-7&ccb=1-7&_nc_sid=58cdad&efg=eyJ2ZW5jb2RlX3RhZyI6InhwaWRzLjY0MHgxMTM2LnNkciJ9&_nc_ohc=mjXPCkkF1BEQ7kNvwFelyVV&_nc_oc=AdkgQSZbSinyAtN9uVIVIFoVwYjJK53PQ_lG-e-JjKvuiYpi26wTgh5XSbI1AItKglA&_nc_ad=z-m&_nc_cid=0&_nc_zt=23&_nc_ht=scontent.cdninstagram.com&_nc_gid=wq3phUaaZcxz05d9N8p1CQ&oh=00_AfSKXEpujxufjLtS3TD1IzmRvIoB6wy6bq0JH3v3TiPmwQ&oe=6885762A"
},
{
"context": "https://www.instagram.com/jokowi/reel/DMFubsQhxmM/",
"name": "content",
"type": "string",
"value": "Pertunjukan spesial dari mahasiswa-mahasiswi UKI Yogyakarta di depan kediaman. Terima kasih atas perhatian dan dukungannya yang hangat!"
},
{
"context": "https://www.instagram.com/jokowi/reel/DLumay5Bx0t/",
"name": "url",
"type": "url",
"value": "https://www.instagram.com/jokowi/reel/DLumay5Bx0t/"
},
{
"context": "https://www.instagram.com/jokowi/reel/DLumay5Bx0t/",
"name": "thumbnailUrl",
"type": "url",
"value": "https://scontent.cdninstagram.com/v/t51.71878-15/516449709_712998244821804_8121518606345055304_n.jpg?stp=dst-jpg_e15_tt6&_nc_cat=110&ig_cache_key=MzY3MDAzOTcxMzQ3MTIwODc0OQ%3D%3D.3-ccb1-7&ccb=1-7&_nc_sid=58cdad&efg=eyJ2ZW5jb2RlX3RhZyI6InhwaWRzLjY0MHgxMTM2LnNkciJ9&_nc_ohc=BUh_kYhiXRkQ7kNvwGT773l&_nc_oc=AdmtFFurX4j0wddMlHXHQBL3o1LjYNen-NEX4IW10pFiQwqvSo503GSRLiea0UXXGvE&_nc_ad=z-m&_nc_cid=0&_nc_zt=23&_nc_ht=scontent.cdninstagram.com&_nc_gid=wq3phUaaZcxz05d9N8p1CQ&oh=00_AfQLiieHKzgC3DTqATTd57PSootEGOdsz655J1KEZhymoQ&oe=6885746E"
},
{
"context": "https://www.instagram.com/jokowi/reel/DLumay5Bx0t/",
"name": "content",
"type": "string",
"value": "Pagi ini saya mencoba naik ATV di pantai bersama cucu-cucu. Ternyata sulit sekali untuk berhenti 😄"
},
{
"context": "https://www.instagram.com/jokowi/p/DLooTsthS_m/",
"name": "url",
"type": "url",
"value": "https://www.instagram.com/jokowi/p/DLooTsthS_m/"
},
{
"context": "https://www.instagram.com/jokowi/p/DLooTsthS_m/",
"name": "thumbnailUrl",
"type": "url",
"value": "https://instagram.fdps17-1.fna.fbcdn.net/v/t51.2885-15/514848573_18487163449075048_432826180196670794_n.webp?efg=eyJ2ZW5jb2RlX3RhZyI6IkNBUk9VU0VMX0lURU0uaW1hZ2VfdXJsZ2VuLjE0NDB4MTkyMC5zZHIuZjgyNzg3LmRlZmF1bHRfaW1hZ2UuYzIifQ&_nc_ht=instagram.fdps17-1.fna.fbcdn.net&_nc_cat=104&_nc_oc=Q6cZ2QFFdz0HZSILtfF6Rzwy-7QTXpo6wKoqXnj-acYERdRgEiC2b09IA6YcYhLsoIcwtnM&_nc_ohc=tc2BcnYHFecQ7kNvwEgTAT8&_nc_gid=wq3phUaaZcxz05d9N8p1CQ&edm=AP4sbd4BAAAA&ccb=7-5&ig_cache_key=MzY2ODM1OTE1OTY0MjE0NTAzNw%3D%3D.3-ccb7-5&oh=00_AfSLMXnOqzAwvenVnnk6b9pbQ4NVMMC3EfSZoP415asR6w&oe=688558A2&_nc_sid=7a9f4b"
},
{
"context": "https://www.instagram.com/jokowi/p/DLooTsthS_m/",
"name": "content",
"type": "string",
"value": "Senang dapat bermain bersama cucu-cucu di pantai saat liburan sekolah. Momen seperti ini sederhana, tapi menyegarkan pikiran dan menenangkan hati."
},
{
"context": "https://www.instagram.com/jokowi/reel/DLbyOP8hJu2/",
"name": "url",
"type": "url",
"value": "https://www.instagram.com/jokowi/reel/DLbyOP8hJu2/"
},
{
"context": "https://www.instagram.com/jokowi/reel/DLbyOP8hJu2/",
"name": "thumbnailUrl",
"type": "url",
"value": "https://scontent.cdninstagram.com/v/t51.71878-15/510435322_3173564999475228_4451451499021688820_n.jpg?stp=dst-jpg_e15_tt6&_nc_cat=111&ig_cache_key=MzY2NDc0MzYwMzMxNTcxMDkwMg%3D%3D.3-ccb1-7&ccb=1-7&_nc_sid=58cdad&efg=eyJ2ZW5jb2RlX3RhZyI6InhwaWRzLjY0MHgxMTM2LnNkciJ9&_nc_ohc=AkQwuEW_EDkQ7kNvwEvFDYy&_nc_oc=AdnMGkD2AfN02pLszomu44YXNAEJRsmBoBpYFPw6zCkjA6zRavvMKon9eqjVDRm8LuY&_nc_ad=z-m&_nc_cid=0&_nc_zt=23&_nc_ht=scontent.cdninstagram.com&_nc_gid=wq3phUaaZcxz05d9N8p1CQ&oh=00_AfSslKc6ESnaZ0WgbyG71KTu8cuAyGcKu2hxD-bSKywLbQ&oe=688575D0"
},
{
"context": "https://www.instagram.com/jokowi/reel/DLbyOP8hJu2/",
"name": "content",
"type": "string",
"value": "Pak Harsudi dari Sukabumi menunjukkan bahwa kepedulian pada lingkungan bisa diwujudkan dengan cara yang sederhana namun penuh makna. Selama hampir lima tahun, beliau mengumpulkan limbah plastik dan merajutnya menjadi karpet daur ulang sepanjang 11 meter. Bukan hanya karya seni, tapi juga simbol ketekunan dan kreativitas.\n\nKarya ini mengingatkan kita bahwa setiap tindakan kecil bisa membawa dampak besar. Dengan niat yang tulus dan proses yang konsisten, bahkan limbah pun bisa menjadi sesuatu yang bernilai dan menginspirasi banyak orang. Terima kasih atas semangat dan dedikasinya, Pak Harsudi."
},
{
"context": "https://www.instagram.com/jokowi/reel/DLY6SUmh5c5/",
"name": "url",
"type": "url",
"value": "https://www.instagram.com/jokowi/reel/DLY6SUmh5c5/"
},
{
"context": "https://www.instagram.com/jokowi/reel/DLY6SUmh5c5/",
"name": "thumbnailUrl",
"type": "url",
"value": "https://scontent.cdninstagram.com/v/t51.71878-15/510435059_1012640944365927_3735622432573815901_n.jpg?stp=dst-jpg_e15_tt6&_nc_cat=111&ig_cache_key=MzY2MzkzNDY0MjYzNTM4MDUzNw%3D%3D.3-ccb1-7&ccb=1-7&_nc_sid=58cdad&efg=eyJ2ZW5jb2RlX3RhZyI6InhwaWRzLjY0MHgxMTM2LnNkciJ9&_nc_ohc=13LX2_NgLBcQ7kNvwGLI9VY&_nc_oc=Adl4-EAsojhjH0WGHAd_qhYVCG7sAFkXgnR41qqFTsCIelQTduNlITFoC3seo3quhYM&_nc_ad=z-m&_nc_cid=0&_nc_zt=23&_nc_ht=scontent.cdninstagram.com&_nc_gid=wq3phUaaZcxz05d9N8p1CQ&oh=00_AfQJIR9uSqyHls1ci1nCXQqHU5sPM9R_u31OHnCI6H6A-A&oe=68854E0C"
},
{
"context": "https://www.instagram.com/jokowi/reel/DLY6SUmh5c5/",
"name": "content",
"type": "string",
"value": "Selamat Tahun Baru Islam 1447 Hijriyah.\nMomen ini mengingatkan kita bahwa perubahan sejati datang dari ketulusan dan kebersamaan.\nMari terus jaga semangat kerukunan dan perdamaian."
},
{
"context": "https://www.instagram.com/jokowi/reel/DLOTHJxBinr/",
"name": "url",
"type": "url",
"value": "https://www.instagram.com/jokowi/reel/DLOTHJxBinr/"
},
{
"context": "https://www.instagram.com/jokowi/reel/DLOTHJxBinr/",
"name": "thumbnailUrl",
"type": "url",
"value": "https://scontent.cdninstagram.com/v/t51.71878-15/509890521_1078717800840222_2399100765814600721_n.jpg?stp=dst-jpg_e15_tt6&_nc_cat=111&ig_cache_key=MzY2MDk0NzYwMTUwNTAwNDAxMQ%3D%3D.3-ccb1-7&ccb=1-7&_nc_sid=58cdad&efg=eyJ2ZW5jb2RlX3RhZyI6InhwaWRzLjY0MHgxMTM2LnNkciJ9&_nc_ohc=dvxpahQ3jxkQ7kNvwG0eACM&_nc_oc=Adk55rv_61TRv88p9UeFgmUy6wfLCNY4EwSNlUsz5H8PwYJ8OpaEoPhYvu8NorJvZCw&_nc_ad=z-m&_nc_cid=0&_nc_zt=23&_nc_ht=scontent.cdninstagram.com&_nc_gid=wq3phUaaZcxz05d9N8p1CQ&oh=00_AfSxRdG_yej7D-r4xhMMU7_DJnUO_wtPdzQj2pPB_GQJ2Q&oe=688559D5"
},
{
"context": "https://www.instagram.com/jokowi/reel/DLOTHJxBinr/",
"name": "content",
"type": "string",
"value": "Terima kasih untuk setiap doa, ucapan, dan karangan bunga.\nSaya dan keluarga merasa sangat tersentuh dan dikuatkan oleh perhatian dan kasih sayang yang diberikan oleh Bapak Presiden @prabowo, para sahabat dan masyarakat dari berbagai penjuru negeri.\n\nSemoga semua kebaikan yang disampaikan dibalas berlipat ganda oleh Tuhan Yang Maha Esa. 🙏"
},
{
"context": "https://www.instagram.com/jokowi/reel/DKjzwc1hu3h/",
"name": "url",
"type": "url",
"value": "https://www.instagram.com/jokowi/reel/DKjzwc1hu3h/"
},
{
"context": "https://www.instagram.com/jokowi/reel/DKjzwc1hu3h/",
"name": "thumbnailUrl",
"type": "url",
"value": "https://scontent.cdninstagram.com/v/t51.71878-15/503869592_1344177693307964_4256342620284667217_n.jpg?stp=dst-jpg_e15_tt6&_nc_cat=103&ig_cache_key=MzY0ODk4Nzc1Mjk2OTk4OTYwMQ%3D%3D.3-ccb1-7&ccb=1-7&_nc_sid=58cdad&efg=eyJ2ZW5jb2RlX3RhZyI6InhwaWRzLjY0MHgxMTM2LnNkciJ9&_nc_ohc=768D6mzr0VcQ7kNvwHr2gaW&_nc_oc=AdnIO2SkIRPGLosYEkkqzBK7eJoCYNXoMLSW1DYc75tjZvOr2eM6T88jDgincWRtyz4&_nc_ad=z-m&_nc_cid=0&_nc_zt=23&_nc_ht=scontent.cdninstagram.com&_nc_gid=wq3phUaaZcxz05d9N8p1CQ&oh=00_AfSc5A5I8n59F7xNrK7bpn3unnzMG_TYG_3xRsBwDV5quA&oe=688576B1"
},
{
"context": "https://www.instagram.com/jokowi/reel/DKjzwc1hu3h/",
"name": "content",
"type": "string",
"value": "Jum'at pagi tadi saya melaksanakan Salat Iduladha di halaman Gedung Graha Saba, Solo, kemudian bersilaturahmi dan beramah tamah bersama warga sekitar.\n\nSelamat merayakan Iduladha bagi seluruh Umat Islam, di mana pun berada."
},
{
"context": "https://www.instagram.com/jokowi/reel/DKimymYh3R0/",
"name": "url",
"type": "url",
"value": "https://www.instagram.com/jokowi/reel/DKimymYh3R0/"
},
{
"context": "https://www.instagram.com/jokowi/reel/DKimymYh3R0/",
"name": "thumbnailUrl",
"type": "url",
"value": "https://scontent.cdninstagram.com/v/t51.71878-15/504107158_1249135363550582_4731561455237896905_n.jpg?stp=dst-jpg_e15_tt6&_nc_cat=100&ig_cache_key=MzY0ODY0OTI1MTA3ODUwMTQ5Mg%3D%3D.3-ccb1-7&ccb=1-7&_nc_sid=58cdad&efg=eyJ2ZW5jb2RlX3RhZyI6InhwaWRzLjY0MHgxMTM2LnNkciJ9&_nc_ohc=u7LQpfNHPKsQ7kNvwGR0CaP&_nc_oc=AdmQ65GSIGuY_rQTVVwajV9vpe25A5BTfLY0y1FAE99b9z6nsaplQkQnCi3i9xMLrzY&_nc_ad=z-m&_nc_cid=0&_nc_zt=23&_nc_ht=scontent.cdninstagram.com&_nc_gid=wq3phUaaZcxz05d9N8p1CQ&oh=00_AfQXIlc1LIdrgRZZuj5hca5U4xSMEe0gmQXPvJFLQwMH1w&oe=688554A3"
},
{
"context": "https://www.instagram.com/jokowi/reel/DKimymYh3R0/",
"name": "content",
"type": "string",
"value": "Semoga semangat berkurban menumbuhkan keikhlasan, kepedulian terhadap sesama, dan keteguhan iman dalam diri kita. Mohon maaf lahir dan batin.\n\nSelamat Hari Raya Iduladha 1446 Hijriah. Semoga Allah SWT senantiasa memberkahi langkah kita semua."
},
{
"context": "https://www.instagram.com/jokowi/reel/DKiV5v1B_XH/",
"name": "url",
"type": "url",
"value": "https://www.instagram.com/jokowi/reel/DKiV5v1B_XH/"
},
{
"context": "https://www.instagram.com/jokowi/reel/DKiV5v1B_XH/",
"name": "thumbnailUrl",
"type": "url",
"value": "https://scontent.cdninstagram.com/v/t51.75761-15/504271573_18482683051075048_3073227615232722894_n.jpg?stp=dst-jpg_e15_tt6&_nc_cat=104&ig_cache_key=MzY0ODU3NDk3NTQ2NjAxMDA1NTE4NDgyNjgzMDQ4MDc1MDQ4.3-ccb1-7&ccb=1-7&_nc_sid=58cdad&efg=eyJ2ZW5jb2RlX3RhZyI6InhwaWRzLjU0MHg5NjAuc2RyIn0%3D&_nc_ohc=uGujMGeVkbUQ7kNvwFolwtb&_nc_oc=Admpr0cVy-TQAu2oBchjp9MVZIPeJNggP6XfeP9pf7xlY-A5CtbGily_of6xkJEJgYo&_nc_ad=z-m&_nc_cid=0&_nc_zt=23&_nc_ht=scontent.cdninstagram.com&_nc_gid=wq3phUaaZcxz05d9N8p1CQ&oh=00_AfTNxK_mkS4aF_9fkKEDmhElDQptpl0xk3A5-T3P1JK74A&oe=68857230"
},
{
"context": "https://www.instagram.com/jokowi/reel/DKiV5v1B_XH/",
"name": "content",
"type": "string",
"value": "Kita bersyukur dan bangga. Terima kasih kepada seluruh pemain dan tim pelatih Timnas Indonesia atas semangat juang dan kerja keras yang luar biasa. Kemenangan atas China bukan hanya hasil di atas lapangan, tetapi juga menjadi bukti bahwa Indonesia berhasil menempati peringkat keempat klasemen grup dan melaju ke putaran keempat kualifikasi Piala Dunia. Selamat untuk Timnas Garuda. Kalian telah mengharumkan nama bangsa."
}
],
"savedAt": "2025-07-22T12:58:32.659Z",
"type": "post",
"url": "https://www.instagram.com/jokowi/"
}
]

View File

@@ -0,0 +1,142 @@
[
{
"facts": [
{
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7351977300806025216",
"name": "url",
"type": "string",
"value": "https://www.linkedin.com/feed/update/urn:li:activity:7351977300806025216"
},
{
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7351977300806025216",
"name": "displayName",
"type": "string",
"value": "Quincy Larson"
},
{
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7351977300806025216",
"name": "content",
"type": "string",
"value": "The freeCodeCamp community is publishing SO MANY books and courses. We're building a life-long learner's paradise. Here are this week's 5 resources worth your time: \n \n1\\. freeCodeCamp just published this comprehensive guide to AI app security and the most common vulnerabilities. You'll learn about Threat Modeling, Prompt Injection, Data Poisoning, supply chain risks, and more. (1 hour YouTube course): [https://lnkd.in/gzEnPDnt](https://lnkd.in/gzEnPDnt) \n \n2\\. JavaScript is the most popular language on the planet. But it's also super duper prone to errors. Luckily, freeCodeCamp just dropped this comprehensive handbook to help you understand how JavaScript's error handling works. You'll learn about Try-Catch, Error Rethrowing, the Finally keyword, and the Error Object itself. We also show tons of example code that you can scrutinize and learn from. (full-length handbook): [https://lnkd.in/gm8tpKu9](https://lnkd.in/gm8tpKu9) \n \n3\\. On this week's podcast, I interview a developer who had to apply to 800 jobs, but eventually landed one. Braydon Coyer started out building mobile apps in high school. At one point his iPhone game even out-sold Angry Birds for a day or two. He shares tons of strategies for applying for developer roles, sane ways to integrate AI into your developer workflows, and how to switch from mobile app dev to full stack dev. This dude is a blast. (1 hour watch or listen in your favorite podcast app): [https://lnkd.in/gGjSbXMw](https://lnkd.in/gGjSbXMw) \n \n4\\. I'm a huge fan of data visualization, and I love me some D3.js. So I was jazzed about this new course that'll help you shore up your Data Viz fundamentals. You'll go from bare-bones scatter plots to dynamically updating charts with fancy animations. If you want to learn how to make your data more accessible and more fun, this course is for you. (90 minute YouTube course): [https://lnkd.in/g6UnAK6M](https://lnkd.in/g6UnAK6M) \n \n5\\. As you may know, freeCodeCamp is a big open source project. And we have tons of developers who jump in to help us improve our curriculum and our codebase. But it's common for many of them to lose steam and drop off the map. This guide will help ensure that this doesn't happen to you. It'll give you actionable tips for setting open source goals, finding the right projects to get involved in, engaging with fellow devs, and more. (15 minute read): [https://lnkd.in/gr-\\_Wexr](https://lnkd.in/gr-_Wexr) \n \nQuote of the Week: \n“When I was learning to code, the big picture didnt click until I needed to solve real problems. Thats when variables and arrays started making sense — not in theory, but in practice.” — Software Engineer Braydon Coyer on this week's freeCodeCamp podcast \n \nIf you read this far... support our charity's mission: [https://lnkd.in/gSGSfE8b](https://lnkd.in/gSGSfE8b) 🏕️\n\nLove this, Quincy. Much needed for me.\n\nThanks for sharing, Quincy\n\nThanks for sharing, Quincy\n\n![Image](https://media.licdn.com/dms/image/v2/D5622AQGCqdEED0k1dQ/feedshare-shrink_800/B56Zgd4ZYjIAAk-/0/1752847980986?e=1756339200&v=beta&t=Fe1phU1c5qBRr48DJ--427KaqqZUxyfhMZyKa_oYys8)"
},
{
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7351977300806025216",
"name": "likeCount",
"type": "number",
"value": "233"
},
{
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7351977300806025216",
"name": "commentCount",
"type": "number",
"value": "3"
},
{
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7351977300806025216",
"name": "shareCount",
"type": "number",
"value": "20"
}
],
"savedAt": "2025-07-22T12:54:19.113Z",
"type": "post",
"url": "https://www.linkedin.com/feed/update/urn:li:activity:7351977300806025216/"
},
{
"facts": [
{
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7351977300806025216/#comment_by=rex-kishore",
"name": "displayName",
"type": "string",
"value": "Kishore Kumar"
},
{
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7351977300806025216/#comment_by=rex-kishore",
"name": "content",
"type": "string",
"value": "Love this, Quincy. Much needed for me."
},
{
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7351977300806025216/#comment_by=rex-kishore",
"name": "likeCount",
"type": "number",
"value": "0"
},
{
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7351977300806025216/#comment_by=rex-kishore",
"name": "commentCount",
"type": "number",
"value": "0"
},
{
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7351977300806025216/#comment_by=rex-kishore",
"name": "createdAt",
"type": "timestr",
"value": "3hari"
},
{
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7351977300806025216/#comment_by=oluwafemi-adebayo-606688159",
"name": "displayName",
"type": "string",
"value": "Oluwafemi Adebayo"
},
{
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7351977300806025216/#comment_by=oluwafemi-adebayo-606688159",
"name": "content",
"type": "string",
"value": "Thanks for sharing, Quincy"
},
{
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7351977300806025216/#comment_by=oluwafemi-adebayo-606688159",
"name": "likeCount",
"type": "number",
"value": "0"
},
{
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7351977300806025216/#comment_by=oluwafemi-adebayo-606688159",
"name": "commentCount",
"type": "number",
"value": "0"
},
{
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7351977300806025216/#comment_by=oluwafemi-adebayo-606688159",
"name": "createdAt",
"type": "timestr",
"value": "1hari"
},
{
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7351977300806025216/#comment_by=daretechie",
"name": "displayName",
"type": "string",
"value": "Dare A."
},
{
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7351977300806025216/#comment_by=daretechie",
"name": "content",
"type": "string",
"value": "Thanks for sharing, Quincy"
},
{
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7351977300806025216/#comment_by=daretechie",
"name": "likeCount",
"type": "number",
"value": "0"
},
{
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7351977300806025216/#comment_by=daretechie",
"name": "commentCount",
"type": "number",
"value": "0"
},
{
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7351977300806025216/#comment_by=daretechie",
"name": "createdAt",
"type": "timestr",
"value": "3hari"
}
],
"savedAt": "2025-07-22T12:54:19.136Z",
"type": "comment",
"url": "https://www.linkedin.com/feed/update/urn:li:activity:7351977300806025216/"
}
]

View File

@@ -0,0 +1,220 @@
[
{
"facts": [
{
"context": "https://www.linkedin.com/in/quincylarson",
"name": "displayName",
"type": "string",
"value": "Quincy Larson"
},
{
"context": "https://www.linkedin.com/in/quincylarson",
"name": "description",
"type": "string",
"value": "Teacher and founder of freeCodeCamp.org 🏕️ Host of the freeCodeCamp Podcast 🎧"
},
{
"context": "https://www.linkedin.com/in/quincylarson",
"name": "profileImage",
"type": "url",
"value": "https://media.licdn.com/dms/image/v2/C5603AQFET2zJ01O5Ug/profile-displayphoto-shrink_100_100/profile-displayphoto-shrink_100_100/0/1617221558171?e=1756339200&v=beta&t=rhCIDfIiPMN-COe0-niFes8YhUdBXBrulQHxCT7pxf4"
},
{
"context": "https://www.linkedin.com/in/quincylarson",
"name": "followerCount",
"type": "number",
"value": "4109"
}
],
"savedAt": "2025-07-22T12:53:23.051Z",
"type": "profile",
"url": "https://www.linkedin.com/in/quincylarson/recent-activity/all/"
},
{
"facts": [
{
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7353277001924837376",
"name": "url",
"type": "string",
"value": "https://www.linkedin.com/feed/update/urn:li:activity:7353277001924837376"
},
{
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7353277001924837376",
"name": "displayName",
"type": "string",
"value": "Quincy Larson"
},
{
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7353277001924837376",
"name": "content",
"type": "string",
"value": "The Tokyo subway system UX is on another level. Check out this handy chart of which train car you should choose for the fastest exit from each destination station.\n\n![Image](https://media.licdn.com/dms/image/v2/D4E22AQEgtdGCmc3b6A/feedshare-shrink_800/B4EZgwWduIGoAg-/0/1753157853212?e=1756339200&v=beta&t=QQT2VPNn_MGEsKDtx-cHrstALBCi7iY1aX3iBeXW5pY)"
},
{
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7353277001924837376",
"name": "likeCount",
"type": "number",
"value": "30"
},
{
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7353277001924837376",
"name": "commentCount",
"type": "number",
"value": "1"
},
{
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7353277001924837376",
"name": "shareCount",
"type": "number",
"value": "1"
},
{
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7351977300806025216",
"name": "url",
"type": "string",
"value": "https://www.linkedin.com/feed/update/urn:li:activity:7351977300806025216"
},
{
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7351977300806025216",
"name": "displayName",
"type": "string",
"value": "Quincy Larson"
},
{
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7351977300806025216",
"name": "content",
"type": "string",
"value": "The freeCodeCamp community is publishing SO MANY books and courses. We're building a life-long learner's paradise. Here are this week's 5 resources worth your time: \n \n1\\. freeCodeCamp just published this comprehensive guide to AI app security and the most common vulnerabilities. You'll learn about Threat Modeling, Prompt Injection, Data Poisoning, supply chain risks, and more. (1 hour YouTube course): [https://lnkd.in/gzEnPDnt](https://lnkd.in/gzEnPDnt) \n \n2\\. JavaScript is the most popular language on the planet. But it's also super duper prone to errors. Luckily, freeCodeCamp just dropped this comprehensive handbook to help you understand how JavaScript's error handling works. You'll learn about Try-Catch, Error Rethrowing, the Finally keyword, and the Error Object itself. We also show tons of example code that you can scrutinize and learn from. (full-length handbook): [https://lnkd.in/gm8tpKu9](https://lnkd.in/gm8tpKu9) \n \n3\\. On this week's podcast, I interview a developer who had to apply to 800 jobs, but eventually landed one. Braydon Coyer started out building mobile apps in high school. At one point his iPhone game even out-sold Angry Birds for a day or two. He shares tons of strategies for applying for developer roles, sane ways to integrate AI into your developer workflows, and how to switch from mobile app dev to full stack dev. This dude is a blast. (1 hour watch or listen in your favorite podcast app): [https://lnkd.in/gGjSbXMw](https://lnkd.in/gGjSbXMw) \n \n4\\. I'm a huge fan of data visualization, and I love me some D3.js. So I was jazzed about this new course that'll help you shore up your Data Viz fundamentals. You'll go from bare-bones scatter plots to dynamically updating charts with fancy animations. If you want to learn how to make your data more accessible and more fun, this course is for you. (90 minute YouTube course): [https://lnkd.in/g6UnAK6M](https://lnkd.in/g6UnAK6M) \n \n5\\. As you may know, freeCodeCamp is a big open source project. And we have tons of developers who jump in to help us improve our curriculum and our codebase. But it's common for many of them to lose steam and drop off the map. This guide will help ensure that this doesn't happen to you. It'll give you actionable tips for setting open source goals, finding the right projects to get involved in, engaging with fellow devs, and more. (15 minute read): [https://lnkd.in/gr-\\_Wexr](https://lnkd.in/gr-_Wexr) \n \nQuote of the Week: \n“When I was learning to code, the big picture didnt click until I needed to solve real problems. Thats when variables and arrays started making sense — not in theory, but in practice.” — Software Engineer Braydon Coyer on this week's freeCodeCamp podcast \n \nIf you read this far... support our charity's mission: [https://lnkd.in/gSGSfE8b](https://lnkd.in/gSGSfE8b) 🏕️\n\n![Image](https://media.licdn.com/dms/image/v2/D5622AQGCqdEED0k1dQ/feedshare-shrink_800/B56Zgd4ZYjIAAk-/0/1752847980986?e=1756339200&v=beta&t=Fe1phU1c5qBRr48DJ--427KaqqZUxyfhMZyKa_oYys8)"
},
{
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7351977300806025216",
"name": "likeCount",
"type": "number",
"value": "233"
},
{
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7351977300806025216",
"name": "commentCount",
"type": "number",
"value": "3"
},
{
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7351977300806025216",
"name": "shareCount",
"type": "number",
"value": "20"
},
{
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7350887231995199490",
"name": "url",
"type": "string",
"value": "https://www.linkedin.com/feed/update/urn:li:activity:7350887231995199490"
},
{
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7350887231995199490",
"name": "displayName",
"type": "string",
"value": "Quincy Larson"
},
{
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7350887231995199490",
"name": "content",
"type": "string",
"value": "I just gave my first-ever presentation completely in Japanese to the Keio Computer Society. 日本語勉強大好き🍣 If youre a native Japanese speaker near Shinjuku and want to language exchange DM me. Im ~N3 level and want to practice more dev-focused terms. 会話しよう。☕️"
},
{
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7350887231995199490",
"name": "likeCount",
"type": "number",
"value": "347"
},
{
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7350887231995199490",
"name": "commentCount",
"type": "number",
"value": "38"
},
{
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7350887231995199490",
"name": "shareCount",
"type": "number",
"value": "1"
},
{
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7349465038635614209",
"name": "url",
"type": "string",
"value": "https://www.linkedin.com/feed/update/urn:li:activity:7349465038635614209"
},
{
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7349465038635614209",
"name": "displayName",
"type": "string",
"value": "Quincy Larson"
},
{
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7349465038635614209",
"name": "content",
"type": "string",
"value": "I just sent out this week's \"5 freeCodeCamp resources worth your time.\" Some heaters in here including a 2-hour course on agentic \"vibe coding\" with n8n. \n \n1\\. freeCodeCamp just published this comprehensive course that will teach you how to build apps using the LAMP Stack Linux, Apache, MySQL, PHP. This is a classic toolchain that a huge number of websites still use to this day. You can code along at home and build your own clone of Google Calendar complete with multi-appointment support and booking conflict logic. Then you'll use JavaScript to add a dynamic user interface. This project will help you expand your horizons and get in some serious reps. (3 hour YouTube course): [https://lnkd.in/gw56n5dr](https://lnkd.in/gw56n5dr) \n \n2\\. You may have heard the term “vibe coding”, where you write the specifications for your app, then hand it over to code generation agents who write the code for you. This approach has been a bit polarizing in the developer community, with vocal critics and vocal advocates. My advice is don't listen to any of them. Think for yourself. freeCodeCamp just published this course that will introduce you to vibe coding tools like n8n an open source workflow automation tool so you can wire together APIs and services without needing to write a ton of code manually. (2 hour YouTube course): [https://lnkd.in/gzM6-9ws](https://lnkd.in/gzM6-9ws) \n \n3\\. On this week's podcast I interview Joe Hill. He's a software engineer who works on a data platform for NASA. Joe taught himself programming for 4 years while working as a janitor. As the single father of two Autistic boys, he first used his programming skills to build an iPad app to help them learn how to talk. He shares tons of practical tips for learning new skills and for getting things done inside big orgs. (1 hour watch or listen in your favorite podcast app): [https://lnkd.in/gsvEUU-E](https://lnkd.in/gsvEUU-E) \n \n4\\. Learn how to write Python tests so you can make your codebase more robust. This tutorial will teach you the basics of pytest and show you tons of examples. You'll learn about Markers, Fixtures, Parameterization, and more. (20 minute read): [https://lnkd.in/gafgJ8nq](https://lnkd.in/gafgJ8nq) \n \n5\\. Tell your gardener friends: freeCodeCamp just published a tutorial on how to monitor the moisture of your soil using an Arduino microcontroller. This tutorial will show you what hardware to get and how to wire it up. Then it'll walk you through the code you'll run, and explain how everything works. (30 minute read): [https://lnkd.in/gggjuHhU](https://lnkd.in/gggjuHhU) \n \nQuote of the Week: \n“Becoming tool agnostic is one of the biggest strengths you will ever have as a programmer. If you walk into a place and they say “we use XYZ”, you can go “Great. I'll learn it.” That is so powerful.” - Joe Hill on this week's podcast\n\n![Image](https://media.licdn.com/dms/image/v2/D5622AQEORoqOyAGGXw/feedshare-shrink_800/B56Zf6LgS9HUAg-/0/1752249010762?e=1756339200&v=beta&t=i6MSgn38LfvuZEXXx1JfdzQwGo5IomNqgJBW8C3CPnc)"
},
{
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7349465038635614209",
"name": "likeCount",
"type": "number",
"value": "193"
},
{
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7349465038635614209",
"name": "commentCount",
"type": "number",
"value": "7"
},
{
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7349465038635614209",
"name": "shareCount",
"type": "number",
"value": "9"
},
{
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7348694351415341056",
"name": "url",
"type": "string",
"value": "https://www.linkedin.com/feed/update/urn:li:activity:7348694351415341056"
},
{
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7348694351415341056",
"name": "displayName",
"type": "string",
"value": "Nielda Karla Melo"
},
{
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7348694351415341056",
"name": "content",
"type": "string",
"value": "Quando comecei a escrever esse post me veio um grande sorriso no rosto. Isso porque eu tô aqui para avisar que o episódio desse mês do [freeCodeCamp](https://www.linkedin.com/company/free-code-camp/) em português é a minha entrevista com o [](/in/teocalvo/)[Téo Calvo](https://www.linkedin.com/in/ACoAAA3X1sABWVV0Ru1ijaPUvBgcXVc7xsZR6Eo). \nEu me diverti tanto nessa conversa! Espero que vocês curtam também. \n \nVem assistir na integra aqui: [https://lnkd.in/dAsgBXVP](https://lnkd.in/dAsgBXVP) \n \nTéo, obrigada pela troca e espero em breve poder conversar de novo! 😃\n\n![Image](https://media.licdn.com/dms/image/v2/D4D05AQHNVrZueTgIEg/videocover-high/B4DZfq66rLHACM-/0/1751993010271?e=1753794000&v=beta&t=ia0dZ95oreLUORtZJgaROO8IcHe6GG1dWvrkGe3bn5k)"
},
{
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7348694351415341056",
"name": "likeCount",
"type": "number",
"value": "59"
},
{
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7348694351415341056",
"name": "commentCount",
"type": "number",
"value": "4"
},
{
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7348694351415341056",
"name": "shareCount",
"type": "number",
"value": "1"
}
],
"savedAt": "2025-07-22T12:53:23.052Z",
"type": "post",
"url": "https://www.linkedin.com/in/quincylarson/recent-activity/all/"
}
]

View File

@@ -0,0 +1,508 @@
[
{
"facts": [
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436",
"name": "displayName",
"type": "string",
"value": "Metro TV"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436",
"name": "content",
"type": "string",
"value": "Kelangkaan air bersih di Gaza, Palestina sebabkan warga harus hidup 4-5 hari tanpa air karena bergantung pada pengiriman truk air. \n \n[**#tiktokmetrotv**](/tag/tiktokmetrotv) [**#tiktokberita**](/tag/tiktokberita) [**#beritaterkini**](/tag/beritaterkini) [**#metrotv**](/tag/metrotv) [**#viral**](/tag/viral) [**#fyp**](/tag/fyp) [**#gaza**](/tag/gaza) [**#air**](/tag/air) [**#airbersih**](/tag/airbersih) [**#warga**](/tag/warga) [**#palestine**](/tag/palestine)"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436",
"name": "likeCount",
"type": "number",
"value": "576"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436",
"name": "commentCount",
"type": "number",
"value": "16"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436",
"name": "createdAt",
"type": "timestr",
"value": "20m ago"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436",
"name": "url",
"type": "url",
"value": "https://www.tiktok.com/@metro_tv/video/7529858656179932436"
}
],
"savedAt": "2025-07-22T12:50:59.763Z",
"type": "post",
"url": "https://www.tiktok.com/@metro_tv/video/7529858656179932436"
},
{
"facts": [
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=aabbaa1207",
"name": "displayName",
"type": "string",
"value": "aba•"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=aabbaa1207",
"name": "username",
"type": "string",
"value": "aabbaa1207"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=aabbaa1207",
"name": "content",
"type": "string",
"value": "Free Palestine 🇵🇸🇵🇸🇵🇸"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=aabbaa1207",
"name": "likeCount",
"type": "number",
"value": "3"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=aabbaa1207",
"name": "createdAt",
"type": "timestr",
"value": "15m ago"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=aditaputra59",
"name": "displayName",
"type": "string",
"value": "putra A nya alicia😋🤭"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=aditaputra59",
"name": "username",
"type": "string",
"value": "aditaputra59"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=aditaputra59",
"name": "content",
"type": "string",
"value": "pada sewot cepet² an komen jir😹🤭"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=aditaputra59",
"name": "likeCount",
"type": "number",
"value": "2"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=aditaputra59",
"name": "createdAt",
"type": "timestr",
"value": "16m ago"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=billieeilishgxxx",
"name": "displayName",
"type": "string",
"value": "S"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=billieeilishgxxx",
"name": "username",
"type": "string",
"value": "billieeilishgxxx"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=billieeilishgxxx",
"name": "content",
"type": "string",
"value": "sementara itu di tanah Papua tidak ada yang perduli"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=billieeilishgxxx",
"name": "likeCount",
"type": "number",
"value": "0"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=billieeilishgxxx",
"name": "createdAt",
"type": "timestr",
"value": "8m ago"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=surayaismail06",
"name": "displayName",
"type": "string",
"value": "suraya"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=surayaismail06",
"name": "username",
"type": "string",
"value": "surayaismail06"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=surayaismail06",
"name": "content",
"type": "string",
"value": "sabar2 Allah bersama kalian saudaraku😭😭😭😭😭"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=surayaismail06",
"name": "likeCount",
"type": "number",
"value": "1"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=surayaismail06",
"name": "createdAt",
"type": "timestr",
"value": "14m ago"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=korizen_sastra45",
"name": "displayName",
"type": "string",
"value": "korizen_sastra"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=korizen_sastra45",
"name": "username",
"type": "string",
"value": "korizen_sastra45"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=korizen_sastra45",
"name": "content",
"type": "string",
"value": "pertama"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=korizen_sastra45",
"name": "likeCount",
"type": "number",
"value": "1"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=korizen_sastra45",
"name": "createdAt",
"type": "timestr",
"value": "20m ago"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=user421553031236",
"name": "displayName",
"type": "string",
"value": "reza"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=user421553031236",
"name": "username",
"type": "string",
"value": "user421553031236"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=user421553031236",
"name": "content",
"type": "string",
"value": "astagfirullah"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=user421553031236",
"name": "likeCount",
"type": "number",
"value": "0"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=user421553031236",
"name": "createdAt",
"type": "timestr",
"value": "9m ago"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=latifh4785",
"name": "displayName",
"type": "string",
"value": "latifh"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=latifh4785",
"name": "username",
"type": "string",
"value": "latifh4785"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=latifh4785",
"name": "content",
"type": "string",
"value": "free palestina"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=latifh4785",
"name": "likeCount",
"type": "number",
"value": "0"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=latifh4785",
"name": "createdAt",
"type": "timestr",
"value": "11m ago"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=nabila_azzahra700",
"name": "displayName",
"type": "string",
"value": "Nabila Azzahra Nst"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=nabila_azzahra700",
"name": "username",
"type": "string",
"value": "nabila_azzahra700"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=nabila_azzahra700",
"name": "content",
"type": "string",
"value": "FREE PALESTINE\nSAVE PALESTINE"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=nabila_azzahra700",
"name": "likeCount",
"type": "number",
"value": "0"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=nabila_azzahra700",
"name": "createdAt",
"type": "timestr",
"value": "4m ago"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=geren096",
"name": "displayName",
"type": "string",
"value": "GREN.FR"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=geren096",
"name": "username",
"type": "string",
"value": "geren096"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=geren096",
"name": "content",
"type": "string",
"value": "ke 4"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=geren096",
"name": "likeCount",
"type": "number",
"value": "0"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=geren096",
"name": "createdAt",
"type": "timestr",
"value": "18m ago"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=korizen_sastra45",
"name": "displayName",
"type": "string",
"value": "korizen_sastra"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=korizen_sastra45",
"name": "username",
"type": "string",
"value": "korizen_sastra45"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=korizen_sastra45",
"name": "content",
"type": "string",
"value": "yess pertama"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=korizen_sastra45",
"name": "likeCount",
"type": "number",
"value": "0"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=korizen_sastra45",
"name": "createdAt",
"type": "timestr",
"value": "20m ago"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=1nuranisa",
"name": "displayName",
"type": "string",
"value": "hamba allah"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=1nuranisa",
"name": "username",
"type": "string",
"value": "1nuranisa"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=1nuranisa",
"name": "content",
"type": "string",
"value": "free palestine"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=1nuranisa",
"name": "likeCount",
"type": "number",
"value": "0"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=1nuranisa",
"name": "createdAt",
"type": "timestr",
"value": "13m ago"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=korizen_sastra45",
"name": "displayName",
"type": "string",
"value": "korizen_sastra"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=korizen_sastra45",
"name": "username",
"type": "string",
"value": "korizen_sastra45"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=korizen_sastra45",
"name": "content",
"type": "string",
"value": "sebelum ribuan"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=korizen_sastra45",
"name": "likeCount",
"type": "number",
"value": "0"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=korizen_sastra45",
"name": "createdAt",
"type": "timestr",
"value": "20m ago"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=abang_ojol30",
"name": "displayName",
"type": "string",
"value": "Herman _MAXIM OJEK"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=abang_ojol30",
"name": "username",
"type": "string",
"value": "abang_ojol30"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=abang_ojol30",
"name": "content",
"type": "string",
"value": "free Israel"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=abang_ojol30",
"name": "likeCount",
"type": "number",
"value": "0"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=abang_ojol30",
"name": "createdAt",
"type": "timestr",
"value": "4m ago"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=the_cello7",
"name": "displayName",
"type": "string",
"value": "CELLO🗿😎"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=the_cello7",
"name": "username",
"type": "string",
"value": "the_cello7"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=the_cello7",
"name": "content",
"type": "string",
"value": "ke 10 cuyyyy🗿😁"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=the_cello7",
"name": "likeCount",
"type": "number",
"value": "1"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=the_cello7",
"name": "commentCount",
"type": "number",
"value": "1"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=the_cello7",
"name": "createdAt",
"type": "timestr",
"value": "12m ago"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=urungkan21",
"name": "displayName",
"type": "string",
"value": "ゆうとみんと"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=urungkan21",
"name": "username",
"type": "string",
"value": "urungkan21"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=urungkan21",
"name": "content",
"type": "string",
"value": "gue yang Habis WD25jt 😹\ntangkap Anisss 😡\nyones Mana yones 😌"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=urungkan21",
"name": "likeCount",
"type": "number",
"value": "0"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=urungkan21",
"name": "createdAt",
"type": "timestr",
"value": "13m ago"
}
],
"savedAt": "2025-07-22T12:50:59.774Z",
"type": "comment",
"url": "https://www.tiktok.com/@metro_tv/video/7529858656179932436"
}
]

View File

@@ -0,0 +1,274 @@
[
{
"facts": [
{
"context": "https://www.tiktok.com/@metro_tv",
"name": "username",
"type": "string",
"value": "metro_tv"
},
{
"context": "https://www.tiktok.com/@metro_tv",
"name": "displayName",
"type": "string",
"value": "Metro TV"
},
{
"context": "https://www.tiktok.com/@metro_tv",
"name": "description",
"type": "string",
"value": "The Official Account of Metro TV"
},
{
"context": "https://www.tiktok.com/@metro_tv",
"name": "image",
"type": "url",
"value": "https://p16-sign-sg.tiktokcdn.com/tos-alisg-avt-0068/7312074011570733058~tplv-tiktokx-cropcenter:1080:1080.jpeg?dr=14579&refresh_token=71d036a5&x-expires=1753358400&x-signature=eW0JTtwT%2BGOp5MhV%2FNuRCE9tDK8%3D&t=4d5b0474&ps=13740610&shp=a5d48078&shcp=81f88b70&idc=my2"
},
{
"context": "https://www.tiktok.com/@metro_tv",
"name": "followerCount",
"type": "number",
"value": "8300000"
},
{
"context": "https://www.tiktok.com/@metro_tv",
"name": "followingCount",
"type": "number",
"value": "5"
},
{
"context": "https://www.tiktok.com/@metro_tv",
"name": "postCount",
"type": "number",
"value": "15700"
},
{
"context": "https://www.tiktok.com/@metro_tv",
"name": "likeCount",
"type": "number",
"value": "260400000"
}
],
"savedAt": "2025-07-22T12:50:22.676Z",
"type": "profile",
"url": "https://www.tiktok.com/@metro_tv"
},
{
"facts": [
{
"context": "https://www.tiktok.com/@metro_tv/video/7529876715401743624",
"name": "url",
"type": "url",
"value": "https://www.tiktok.com/@metro_tv/video/7529876715401743624"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436",
"name": "url",
"type": "url",
"value": "https://www.tiktok.com/@metro_tv/video/7529858656179932436"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529877544443596052",
"name": "url",
"type": "url",
"value": "https://www.tiktok.com/@metro_tv/video/7529877544443596052"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529876838890441991",
"name": "url",
"type": "url",
"value": "https://www.tiktok.com/@metro_tv/video/7529876838890441991"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529876619029237013",
"name": "url",
"type": "url",
"value": "https://www.tiktok.com/@metro_tv/video/7529876619029237013"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529873788788378886",
"name": "url",
"type": "url",
"value": "https://www.tiktok.com/@metro_tv/video/7529873788788378886"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529868651105651975",
"name": "url",
"type": "url",
"value": "https://www.tiktok.com/@metro_tv/video/7529868651105651975"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858317242469639",
"name": "url",
"type": "url",
"value": "https://www.tiktok.com/@metro_tv/video/7529858317242469639"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858529126026516",
"name": "url",
"type": "url",
"value": "https://www.tiktok.com/@metro_tv/video/7529858529126026516"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529858036026985749",
"name": "url",
"type": "url",
"value": "https://www.tiktok.com/@metro_tv/video/7529858036026985749"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529857576016612613",
"name": "url",
"type": "url",
"value": "https://www.tiktok.com/@metro_tv/video/7529857576016612613"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529848682011708688",
"name": "url",
"type": "url",
"value": "https://www.tiktok.com/@metro_tv/video/7529848682011708688"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529843661165563143",
"name": "url",
"type": "url",
"value": "https://www.tiktok.com/@metro_tv/video/7529843661165563143"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529843648142363905",
"name": "url",
"type": "url",
"value": "https://www.tiktok.com/@metro_tv/video/7529843648142363905"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529843258269224209",
"name": "url",
"type": "url",
"value": "https://www.tiktok.com/@metro_tv/video/7529843258269224209"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529843252602686741",
"name": "url",
"type": "url",
"value": "https://www.tiktok.com/@metro_tv/video/7529843252602686741"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529827885771705620",
"name": "url",
"type": "url",
"value": "https://www.tiktok.com/@metro_tv/video/7529827885771705620"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529827374465076501",
"name": "url",
"type": "url",
"value": "https://www.tiktok.com/@metro_tv/video/7529827374465076501"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529812013032246548",
"name": "url",
"type": "url",
"value": "https://www.tiktok.com/@metro_tv/video/7529812013032246548"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529805928460209429",
"name": "url",
"type": "url",
"value": "https://www.tiktok.com/@metro_tv/video/7529805928460209429"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529827622180703509",
"name": "url",
"type": "url",
"value": "https://www.tiktok.com/@metro_tv/video/7529827622180703509"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529826652558298385",
"name": "url",
"type": "url",
"value": "https://www.tiktok.com/@metro_tv/video/7529826652558298385"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529825206412561670",
"name": "url",
"type": "url",
"value": "https://www.tiktok.com/@metro_tv/video/7529825206412561670"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529821248117820688",
"name": "url",
"type": "url",
"value": "https://www.tiktok.com/@metro_tv/video/7529821248117820688"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529818903556541717",
"name": "url",
"type": "url",
"value": "https://www.tiktok.com/@metro_tv/video/7529818903556541717"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529805726156426514",
"name": "url",
"type": "url",
"value": "https://www.tiktok.com/@metro_tv/video/7529805726156426514"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529754318732414229",
"name": "url",
"type": "url",
"value": "https://www.tiktok.com/@metro_tv/video/7529754318732414229"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529805514620947733",
"name": "url",
"type": "url",
"value": "https://www.tiktok.com/@metro_tv/video/7529805514620947733"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529807347103862024",
"name": "url",
"type": "url",
"value": "https://www.tiktok.com/@metro_tv/video/7529807347103862024"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529796779689184532",
"name": "url",
"type": "url",
"value": "https://www.tiktok.com/@metro_tv/video/7529796779689184532"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529752657255288084",
"name": "url",
"type": "url",
"value": "https://www.tiktok.com/@metro_tv/video/7529752657255288084"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529796521202666772",
"name": "url",
"type": "url",
"value": "https://www.tiktok.com/@metro_tv/video/7529796521202666772"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529789760177442104",
"name": "url",
"type": "url",
"value": "https://www.tiktok.com/@metro_tv/video/7529789760177442104"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529752692411878677",
"name": "url",
"type": "url",
"value": "https://www.tiktok.com/@metro_tv/video/7529752692411878677"
},
{
"context": "https://www.tiktok.com/@metro_tv/video/7529756053068631312",
"name": "url",
"type": "url",
"value": "https://www.tiktok.com/@metro_tv/video/7529756053068631312"
}
],
"savedAt": "2025-07-22T12:50:22.680Z",
"type": "post",
"url": "https://www.tiktok.com/@metro_tv"
}
]

View File

@@ -0,0 +1,292 @@
[
{
"facts": [
{
"context": "https://x.com/jokowi/status/1947327953580196042",
"name": "url",
"type": "url",
"value": "https://x.com/jokowi/status/1947327953580196042"
},
{
"context": "https://x.com/jokowi/status/1947327953580196042",
"name": "displayName",
"type": "string",
"value": "Joko Widodo"
},
{
"context": "https://x.com/jokowi/status/1947327953580196042",
"name": "username",
"type": "string",
"value": "jokowi"
},
{
"context": "https://x.com/jokowi/status/1947327953580196042",
"name": "content",
"type": "string",
"value": "Saya melepas kepulangan Bapak Presiden Prabowo di Bandara Adi Soemarmo. Semoga lancar dan aman sampai tujuan. 1:41 1:41"
},
{
"context": "https://x.com/jokowi/status/1947327953580196042",
"name": "likeCount",
"type": "number",
"value": "1389"
},
{
"context": "https://x.com/jokowi/status/1947327953580196042",
"name": "shareCount",
"type": "number",
"value": "176"
},
{
"context": "https://x.com/jokowi/status/1947327953580196042",
"name": "commentCount",
"type": "number",
"value": "282"
},
{
"context": "https://x.com/jokowi/status/1947327953580196042",
"name": "bookmarkCount",
"type": "number",
"value": "26"
},
{
"context": "https://x.com/jokowi/status/1947327953580196042",
"name": "viewCount",
"type": "number",
"value": "111728"
},
{
"context": "https://x.com/jokowi/status/1947327953580196042",
"name": "createdAt",
"type": "datetime",
"value": "2025-07-21T16:08:58.000Z"
}
],
"savedAt": "2025-07-22T12:49:01.646Z",
"type": "post",
"url": "https://x.com/jokowi/status/1947327953580196042"
},
{
"facts": [
{
"context": "https://x.com/megapand137/status/1947637749789683895",
"name": "url",
"type": "url",
"value": "https://x.com/megapand137/status/1947637749789683895"
},
{
"context": "https://x.com/megapand137/status/1947637749789683895",
"name": "createdAt",
"type": "datetime",
"value": "2025-07-22T12:39:59.000Z"
},
{
"context": "https://x.com/megapand137/status/1947637749789683895",
"name": "displayName",
"type": "string",
"value": "Megapand137"
},
{
"context": "https://x.com/megapand137/status/1947637749789683895",
"name": "username",
"type": "string",
"value": "megapand137"
},
{
"context": "https://x.com/megapand137/status/1947637749789683895",
"name": "content",
"type": "string",
"value": "Jokowi merasa dihina sehina hinanya katanya atas polemik Ijazahnya...Bukannya bapak selama jadi Presiden sering melakukan pekerjaan pekerjaan hina pak ? Contoh kecilnya...Ngomongnya \" apa \" , tapi yg dikerjakan \"apa \" Apakah iyu tidak hina ?"
},
{
"context": "https://x.com/megapand137/status/1947637749789683895",
"name": "likeCount",
"type": "number",
"value": "2"
},
{
"context": "https://x.com/megapand137/status/1947637749789683895",
"name": "shareCount",
"type": "number",
"value": "0"
},
{
"context": "https://x.com/megapand137/status/1947637749789683895",
"name": "commentCount",
"type": "number",
"value": "0"
},
{
"context": "https://x.com/megapand137/status/1947637749789683895",
"name": "viewCount",
"type": "number",
"value": "69"
},
{
"context": "https://x.com/Rojikin14775945/status/1947633517837619228",
"name": "url",
"type": "url",
"value": "https://x.com/Rojikin14775945/status/1947633517837619228"
},
{
"context": "https://x.com/Rojikin14775945/status/1947633517837619228",
"name": "createdAt",
"type": "datetime",
"value": "2025-07-22T12:23:10.000Z"
},
{
"context": "https://x.com/Rojikin14775945/status/1947633517837619228",
"name": "displayName",
"type": "string",
"value": "Izasah Palsu"
},
{
"context": "https://x.com/Rojikin14775945/status/1947633517837619228",
"name": "username",
"type": "string",
"value": "Rojikin14775945"
},
{
"context": "https://x.com/Rojikin14775945/status/1947633517837619228",
"name": "content",
"type": "string",
"value": "Wi lu kapan digantung atas kebohonganya"
},
{
"context": "https://x.com/Rojikin14775945/status/1947633517837619228",
"name": "likeCount",
"type": "number",
"value": "0"
},
{
"context": "https://x.com/Rojikin14775945/status/1947633517837619228",
"name": "shareCount",
"type": "number",
"value": "0"
},
{
"context": "https://x.com/Rojikin14775945/status/1947633517837619228",
"name": "commentCount",
"type": "number",
"value": "0"
},
{
"context": "https://x.com/Rojikin14775945/status/1947633517837619228",
"name": "viewCount",
"type": "number",
"value": "176"
},
{
"context": "https://x.com/LukkiLw/status/1947627219519738173",
"name": "url",
"type": "url",
"value": "https://x.com/LukkiLw/status/1947627219519738173"
},
{
"context": "https://x.com/LukkiLw/status/1947627219519738173",
"name": "createdAt",
"type": "datetime",
"value": "2025-07-22T11:58:08.000Z"
},
{
"context": "https://x.com/LukkiLw/status/1947627219519738173",
"name": "displayName",
"type": "string",
"value": "Lukki Lw"
},
{
"context": "https://x.com/LukkiLw/status/1947627219519738173",
"name": "username",
"type": "string",
"value": "LukkiLw"
},
{
"context": "https://x.com/LukkiLw/status/1947627219519738173",
"name": "content",
"type": "string",
"value": "kupas tuntas 'kebaikan joko Widodo\n\n[@jokowi](/jokowi)"
},
{
"context": "https://x.com/LukkiLw/status/1947627219519738173",
"name": "likeCount",
"type": "number",
"value": "1"
},
{
"context": "https://x.com/LukkiLw/status/1947627219519738173",
"name": "shareCount",
"type": "number",
"value": "1"
},
{
"context": "https://x.com/LukkiLw/status/1947627219519738173",
"name": "commentCount",
"type": "number",
"value": "0"
},
{
"context": "https://x.com/LukkiLw/status/1947627219519738173",
"name": "viewCount",
"type": "number",
"value": "593"
},
{
"context": "https://x.com/irmawindya/status/1947391677691490518",
"name": "url",
"type": "url",
"value": "https://x.com/irmawindya/status/1947391677691490518"
},
{
"context": "https://x.com/irmawindya/status/1947391677691490518",
"name": "createdAt",
"type": "datetime",
"value": "2025-07-21T20:22:11.000Z"
},
{
"context": "https://x.com/irmawindya/status/1947391677691490518",
"name": "displayName",
"type": "string",
"value": "/juːˈɪ.ə/"
},
{
"context": "https://x.com/irmawindya/status/1947391677691490518",
"name": "username",
"type": "string",
"value": "irmawindya"
},
{
"context": "https://x.com/irmawindya/status/1947391677691490518",
"name": "content",
"type": "string",
"value": "![😬](https://abs-0.twimg.com/emoji/v2/svg/1f62c.svg \"Grimacing face\") ![Image](https://pbs.twimg.com/media/GwaGZ2haYAAQcHW?format=jpg&name=small)"
},
{
"context": "https://x.com/irmawindya/status/1947391677691490518",
"name": "likeCount",
"type": "number",
"value": "156"
},
{
"context": "https://x.com/irmawindya/status/1947391677691490518",
"name": "shareCount",
"type": "number",
"value": "24"
},
{
"context": "https://x.com/irmawindya/status/1947391677691490518",
"name": "commentCount",
"type": "number",
"value": "4"
},
{
"context": "https://x.com/irmawindya/status/1947391677691490518",
"name": "viewCount",
"type": "number",
"value": "7793"
}
],
"savedAt": "2025-07-22T12:49:01.652Z",
"type": "comment",
"url": "https://x.com/jokowi/status/1947327953580196042"
}
]

View File

@@ -0,0 +1,240 @@
[
{
"facts": [
{
"context": "https://x.com/jokowi",
"name": "username",
"type": "string",
"value": "jokowi"
},
{
"context": "https://x.com/jokowi",
"name": "displayName",
"type": "string",
"value": "Joko Widodo"
},
{
"context": "https://x.com/jokowi",
"name": "description",
"type": "string",
"value": "Akun Twitter resmi Joko Widodo 🇮🇩."
},
{
"context": "https://x.com/jokowi",
"name": "profileImage",
"type": "url",
"value": "https://pbs.twimg.com/profile_images/1646769127493877761/bGdslGTd_400x400.jpg"
},
{
"context": "https://x.com/jokowi",
"name": "followerCount",
"type": "number",
"value": "21905953"
},
{
"context": "https://x.com/jokowi",
"name": "followingCount",
"type": "number",
"value": "59"
},
{
"context": "https://x.com/jokowi",
"name": "postCount",
"type": "number",
"value": "7025"
}
],
"savedAt": "2025-07-22T12:48:10.097Z",
"type": "profile",
"url": "https://x.com/jokowi"
},
{
"facts": [
{
"context": "https://x.com/jokowi/status/1947327953580196042",
"name": "url",
"type": "url",
"value": "https://x.com/jokowi/status/1947327953580196042"
},
{
"context": "https://x.com/jokowi/status/1947327953580196042",
"name": "displayName",
"type": "string",
"value": "Joko Widodo"
},
{
"context": "https://x.com/jokowi/status/1947327953580196042",
"name": "username",
"type": "string",
"value": "jokowi"
},
{
"context": "https://x.com/jokowi/status/1947327953580196042",
"name": "content",
"type": "string",
"value": "Saya melepas kepulangan Bapak Presiden Prabowo di Bandara Adi Soemarmo. Semoga lancar dan aman sampai tujuan. ![](https://pbs.twimg.com/ext_tw_video_thumb/1947327709908172800/pu/img/O8G8IycYawWAMF_C.jpg) ![](https://pbs.twimg.com/ext_tw_video_thumb/1947327709908172800/pu/img/O8G8IycYawWAMF_C.jpg)"
},
{
"context": "https://x.com/jokowi/status/1947327953580196042",
"name": "likeCount",
"type": "number",
"value": "1388"
},
{
"context": "https://x.com/jokowi/status/1947327953580196042",
"name": "shareCount",
"type": "number",
"value": "176"
},
{
"context": "https://x.com/jokowi/status/1947327953580196042",
"name": "commentCount",
"type": "number",
"value": "282"
},
{
"context": "https://x.com/jokowi/status/1947327953580196042",
"name": "bookmarkCount",
"type": "number",
"value": "26"
},
{
"context": "https://x.com/jokowi/status/1947327953580196042",
"name": "viewCount",
"type": "number",
"value": "111669"
},
{
"context": "https://x.com/jokowi/status/1947327953580196042",
"name": "createdAt",
"type": "datetime",
"value": "2025-07-21T16:08:58.000Z"
},
{
"context": "https://x.com/jokowi/status/1947194422426517998",
"name": "url",
"type": "url",
"value": "https://x.com/jokowi/status/1947194422426517998"
},
{
"context": "https://x.com/jokowi/status/1947194422426517998",
"name": "displayName",
"type": "string",
"value": "Joko Widodo"
},
{
"context": "https://x.com/jokowi/status/1947194422426517998",
"name": "username",
"type": "string",
"value": "jokowi"
},
{
"context": "https://x.com/jokowi/status/1947194422426517998",
"name": "content",
"type": "string",
"value": "Saya mengajak Bapak Presiden Prabowo menikmati hidangan Bakmi Bu Citro di Solo. Rumah makan sederhana yang punya rasa istimewa. Kami duduk santai, berbincang ringan ditemani semangkuk bakmi hangat. Sebuah momen personal, jauh dari agenda kenegaraan, dan berkesan. Terima kasih Pak Presiden ![](https://pbs.twimg.com/ext_tw_video_thumb/1947194326838136832/pu/img/lNC1SMERg5j2WoTE.jpg) ![](https://pbs.twimg.com/ext_tw_video_thumb/1947194326838136832/pu/img/lNC1SMERg5j2WoTE.jpg)"
},
{
"context": "https://x.com/jokowi/status/1947194422426517998",
"name": "likeCount",
"type": "number",
"value": "1755"
},
{
"context": "https://x.com/jokowi/status/1947194422426517998",
"name": "shareCount",
"type": "number",
"value": "253"
},
{
"context": "https://x.com/jokowi/status/1947194422426517998",
"name": "commentCount",
"type": "number",
"value": "265"
},
{
"context": "https://x.com/jokowi/status/1947194422426517998",
"name": "bookmarkCount",
"type": "number",
"value": "43"
},
{
"context": "https://x.com/jokowi/status/1947194422426517998",
"name": "viewCount",
"type": "number",
"value": "134879"
},
{
"context": "https://x.com/jokowi/status/1947194422426517998",
"name": "createdAt",
"type": "datetime",
"value": "2025-07-21T07:18:22.000Z"
},
{
"context": "https://x.com/jokowi/status/1946955377771450714",
"name": "url",
"type": "url",
"value": "https://x.com/jokowi/status/1946955377771450714"
},
{
"context": "https://x.com/jokowi/status/1946955377771450714",
"name": "displayName",
"type": "string",
"value": "Joko Widodo"
},
{
"context": "https://x.com/jokowi/status/1946955377771450714",
"name": "username",
"type": "string",
"value": "jokowi"
},
{
"context": "https://x.com/jokowi/status/1946955377771450714",
"name": "content",
"type": "string",
"value": "Terima kasih Bapak Presiden Prabowo yang telah berkenan berkunjung kerumah keluarga kami di Solo ![](https://pbs.twimg.com/ext_tw_video_thumb/1946955213375504384/pu/img/88Kmlo4g90ptYgMA.jpg) ![](https://pbs.twimg.com/ext_tw_video_thumb/1946955213375504384/pu/img/88Kmlo4g90ptYgMA.jpg)"
},
{
"context": "https://x.com/jokowi/status/1946955377771450714",
"name": "likeCount",
"type": "number",
"value": "3117"
},
{
"context": "https://x.com/jokowi/status/1946955377771450714",
"name": "shareCount",
"type": "number",
"value": "534"
},
{
"context": "https://x.com/jokowi/status/1946955377771450714",
"name": "commentCount",
"type": "number",
"value": "355"
},
{
"context": "https://x.com/jokowi/status/1946955377771450714",
"name": "bookmarkCount",
"type": "number",
"value": "81"
},
{
"context": "https://x.com/jokowi/status/1946955377771450714",
"name": "viewCount",
"type": "number",
"value": "335996"
},
{
"context": "https://x.com/jokowi/status/1946955377771450714",
"name": "createdAt",
"type": "datetime",
"value": "2025-07-20T15:28:29.000Z"
}
],
"savedAt": "2025-07-22T12:48:10.098Z",
"type": "post",
"url": "https://x.com/jokowi"
}
]

View File

@@ -0,0 +1,664 @@
[
{
"facts": [
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o",
"name": "title",
"type": "string",
"value": "Kumpulan Skak Balik Jokowi ke Prabowo di Debat Keempat Pilpres 2019"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o",
"name": "description",
"type": "string",
"value": "Untuk melihat video-video menarik lainnya kunjungi: https://video.medcom.id/ Dua kandidat capres Joko Widodo dan Prabowo Subianto beradu gagasan dalam debat keempat Pilpres 2019. Debat kali ini mengusung tema ideologi, pemerintahan, keamanan serta hubungan internasional. Siapa unggul pada debat kali ini?"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o",
"name": "date",
"type": "datetime",
"value": "2019-03-30T11:41:18-07:00"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o",
"name": "duration",
"type": "timestr",
"value": "PT1303S"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o",
"name": "thumbnail",
"type": "url",
"value": "https://i.ytimg.com/vi/_joLYHj0i3o/hqdefault.jpg"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o",
"name": "viewCount",
"type": "number",
"value": "10535799"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o",
"name": "likeCount",
"type": "number",
"value": "94857"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV",
"name": "commentCount",
"type": "number",
"value": "32029"
}
],
"savedAt": "2025-07-22T12:57:02.567Z",
"type": "post",
"url": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV"
},
{
"facts": [
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@hambaallah5591",
"name": "username",
"type": "string",
"value": "@hambaallah5591"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@hambaallah5591",
"name": "content",
"type": "string",
"value": "Jangan lupa p prawono tgl 17april hadir dalam pelantikan bpk jokowi untuk 2 preode yang setuju mana like nya"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@hambaallah5591",
"name": "likeCount",
"type": "number",
"value": "467"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@hambaallah5591",
"name": "commentCount",
"type": "number",
"value": "8"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@hambaallah5591",
"name": "createdAt",
"type": "timestr",
"value": "6 tahun yang lalu"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@aztycancer7759",
"name": "username",
"type": "string",
"value": "@aztycancer7759"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@aztycancer7759",
"name": "content",
"type": "string",
"value": "Hhhhhhhh secara tdk langsung mengakui klo Dia non.....???.\nMulai Emosi ya pak..... Smgt Bpk jokowi rakyat yg cerdas pasti mendukung bpk"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@aztycancer7759",
"name": "likeCount",
"type": "number",
"value": "356"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@aztycancer7759",
"name": "commentCount",
"type": "number",
"value": "10"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@aztycancer7759",
"name": "createdAt",
"type": "timestr",
"value": "6 tahun yang lalu"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@sasmithanadra3103",
"name": "username",
"type": "string",
"value": "@sasmithanadra3103"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@sasmithanadra3103",
"name": "content",
"type": "string",
"value": "Inti Debat ,\nPrabowo Maha benar,kemudiaan yg dia lakukan menyalahkan Pak Jokowi.\nKarena itu,sbg Imbalan.\nSaya Coblos No.01\nYg setuju 1..\n#LIKE 🖒"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@sasmithanadra3103",
"name": "likeCount",
"type": "number",
"value": "205"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@sasmithanadra3103",
"name": "commentCount",
"type": "number",
"value": "2"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@sasmithanadra3103",
"name": "createdAt",
"type": "timestr",
"value": "6 tahun yang lalu"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@عرفانالزهري",
"name": "username",
"type": "string",
"value": "@عرفانالزهري"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@عرفانالزهري",
"name": "content",
"type": "string",
"value": "Siapa yg disini mampir lagi setelah melihat debat semalam"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@عرفانالزهري",
"name": "likeCount",
"type": "number",
"value": "84"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@عرفانالزهري",
"name": "commentCount",
"type": "number",
"value": "7"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@عرفانالزهري",
"name": "createdAt",
"type": "timestr",
"value": "1 tahun yang lalu"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@naylaandini8808",
"name": "username",
"type": "string",
"value": "@naylaandini8808"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@naylaandini8808",
"name": "content",
"type": "string",
"value": "Jokowi = like\nPrabowo = komen"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@naylaandini8808",
"name": "likeCount",
"type": "number",
"value": "7200"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@naylaandini8808",
"name": "commentCount",
"type": "number",
"value": "116"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@naylaandini8808",
"name": "createdAt",
"type": "timestr",
"value": "6 tahun yang lalu"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@andrialdytaqwa9291",
"name": "username",
"type": "string",
"value": "@andrialdytaqwa9291"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@andrialdytaqwa9291",
"name": "content",
"type": "string",
"value": "militer indonesia masuk 15 besar di dunia masih dibilang rapuh\n\n\nlike kalau setuju"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@andrialdytaqwa9291",
"name": "likeCount",
"type": "number",
"value": "338"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@andrialdytaqwa9291",
"name": "commentCount",
"type": "number",
"value": "19"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@andrialdytaqwa9291",
"name": "createdAt",
"type": "timestr",
"value": "6 tahun yang lalu"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@pegijasnil1596",
"name": "username",
"type": "string",
"value": "@pegijasnil1596"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@pegijasnil1596",
"name": "content",
"type": "string",
"value": "Pak jokowi memang cocok jadi presiden...soalnya pak jokowi membuat nengara ini semakin semangat maju. Maaf pak dulu saya gak milih bapak tapi sakarang saya pilih bapak"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@pegijasnil1596",
"name": "likeCount",
"type": "number",
"value": "260"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@pegijasnil1596",
"name": "commentCount",
"type": "number",
"value": "2"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@pegijasnil1596",
"name": "createdAt",
"type": "timestr",
"value": "6 tahun yang lalu"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@davidabhafidz3556",
"name": "username",
"type": "string",
"value": "@davidabhafidz3556"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@davidabhafidz3556",
"name": "content",
"type": "string",
"value": "Ada ga yg nonton ini di bulan januari 2024"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@davidabhafidz3556",
"name": "likeCount",
"type": "number",
"value": "121"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@davidabhafidz3556",
"name": "commentCount",
"type": "number",
"value": "35"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@davidabhafidz3556",
"name": "createdAt",
"type": "timestr",
"value": "1 tahun yang lalu"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@merchandisingawesome2600",
"name": "username",
"type": "string",
"value": "@merchandisingawesome2600"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@merchandisingawesome2600",
"name": "content",
"type": "string",
"value": "Ini seperti curhatan warga ke president \nWarga = Prabowo\nPresiden = Pak Jokowi"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@merchandisingawesome2600",
"name": "likeCount",
"type": "number",
"value": "138"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@merchandisingawesome2600",
"name": "commentCount",
"type": "number",
"value": "5"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@merchandisingawesome2600",
"name": "createdAt",
"type": "timestr",
"value": "6 tahun yang lalu"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@rezanajwa9999",
"name": "username",
"type": "string",
"value": "@rezanajwa9999"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@rezanajwa9999",
"name": "content",
"type": "string",
"value": "Prabowo terus bicara masa lalunya(bicata ttg tentara terus).sementara jokowi bicara ttg pencapaian yg sdh di lakukan juga bicara masa depan..sangat kontras...(sy tdk mendengar jokowi bicara ttg usahanya)"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@rezanajwa9999",
"name": "likeCount",
"type": "number",
"value": "373"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@rezanajwa9999",
"name": "commentCount",
"type": "number",
"value": "13"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@rezanajwa9999",
"name": "createdAt",
"type": "timestr",
"value": "6 tahun yang lalu"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@sukasukabro1137",
"name": "username",
"type": "string",
"value": "@sukasukabro1137"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@sukasukabro1137",
"name": "content",
"type": "string",
"value": "saya nangkapnya kog, malah prabowo bukan memberi gagasan untuk indonesia, tapi menyangkal semua gagasan/ ide2 nya pak jokowi... semua nya di sangkal , di salahkan, tnpa membrikan gagasan yg lebih baik\n\nyg setuju like"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@sukasukabro1137",
"name": "likeCount",
"type": "number",
"value": "156"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@sukasukabro1137",
"name": "commentCount",
"type": "number",
"value": "7"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@sukasukabro1137",
"name": "createdAt",
"type": "timestr",
"value": "6 tahun yang lalu"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@alfarezialamsyah3975",
"name": "username",
"type": "string",
"value": "@alfarezialamsyah3975"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@alfarezialamsyah3975",
"name": "content",
"type": "string",
"value": "Ini yang namanya debat . Ngga kaya debat tahun 2024 yang menjatuhkan dan merendahkan."
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@alfarezialamsyah3975",
"name": "likeCount",
"type": "number",
"value": "17"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@alfarezialamsyah3975",
"name": "commentCount",
"type": "number",
"value": "0"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@alfarezialamsyah3975",
"name": "createdAt",
"type": "timestr",
"value": "1 tahun yang lalu"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@IKEWAKI666",
"name": "username",
"type": "string",
"value": "@IKEWAKI666"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@IKEWAKI666",
"name": "content",
"type": "string",
"value": "Pak Wowo bahasanya muter\" diperang trs\nPak owi bahasnya sana sini lebih luas :v\nTrs ku lihat keknya Prabowo gk mikir buat kedepan nya bakal gimana kalo lakuin itu,\nPak Jokowi lebih bekerjasama dengan negara lain lebih berteman,pak Prabowo malah pengen Indonesia itu kek lakuin segalanya sendiri....kapan negara kita maju :v kapan negara kita bersaing kalo Indonesia cuma melakukan segalanya sendiri :v\nDunia ini bukan cuma perang :v"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@IKEWAKI666",
"name": "likeCount",
"type": "number",
"value": "290"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@IKEWAKI666",
"name": "commentCount",
"type": "number",
"value": "13"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@IKEWAKI666",
"name": "createdAt",
"type": "timestr",
"value": "6 tahun yang lalu (diedit)"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@rafafebryan5626",
"name": "username",
"type": "string",
"value": "@rafafebryan5626"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@rafafebryan5626",
"name": "content",
"type": "string",
"value": "Ada 3 golongan yg susah di nasehati\n\n1. Orang yg sedang jatuh cinta \n2. Orang yg sedang emosi\n3. #02"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@rafafebryan5626",
"name": "likeCount",
"type": "number",
"value": "647"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@rafafebryan5626",
"name": "commentCount",
"type": "number",
"value": "33"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@rafafebryan5626",
"name": "createdAt",
"type": "timestr",
"value": "6 tahun yang lalu"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@sarajevo2469",
"name": "username",
"type": "string",
"value": "@sarajevo2469"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@sarajevo2469",
"name": "content",
"type": "string",
"value": "Tahun 1975 pak prabowo bilang ikut perang ke timor timor, gak salah pak!! Bukanya bapak wktu itu malah diasingkan ke bali, itu fakta lhoo pak"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@sarajevo2469",
"name": "likeCount",
"type": "number",
"value": "460"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@sarajevo2469",
"name": "commentCount",
"type": "number",
"value": "9"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@sarajevo2469",
"name": "createdAt",
"type": "timestr",
"value": "6 tahun yang lalu"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@LFchannel-bb4uu",
"name": "username",
"type": "string",
"value": "@LFchannel-bb4uu"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@LFchannel-bb4uu",
"name": "content",
"type": "string",
"value": "Hebatnya pak Jokowi..memilih pak prabowo sebagai menhanx yang benar2 mengetahui kondisi pertahanan dan keamanan negara kita...dan itu di buktikan oleh pak prabowo selama 5 tahun.. dan pak prabowo jg mengakui banyak belajar dr pak jokowi...mereka ini benar2 orang hebat yg di ciptakan untuk indonesia 🫡"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@LFchannel-bb4uu",
"name": "likeCount",
"type": "number",
"value": "36"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@LFchannel-bb4uu",
"name": "commentCount",
"type": "number",
"value": "3"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@LFchannel-bb4uu",
"name": "createdAt",
"type": "timestr",
"value": "1 tahun yang lalu"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@cici3644",
"name": "username",
"type": "string",
"value": "@cici3644"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@cici3644",
"name": "content",
"type": "string",
"value": "pilih prabowo hancur ~> komen\npilih jokowi makmur ~> like\n\nRakyat indonesia makin cerdas"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@cici3644",
"name": "likeCount",
"type": "number",
"value": "3200"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@cici3644",
"name": "commentCount",
"type": "number",
"value": "84"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@cici3644",
"name": "createdAt",
"type": "timestr",
"value": "6 tahun yang lalu"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@surya247766",
"name": "username",
"type": "string",
"value": "@surya247766"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@surya247766",
"name": "content",
"type": "string",
"value": "Beda kelas brow... Meskipun pak dhe dibilang plonga plongo tapi kecerdasannya melebihi rivalnya..."
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@surya247766",
"name": "likeCount",
"type": "number",
"value": "3500"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@surya247766",
"name": "commentCount",
"type": "number",
"value": "204"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@surya247766",
"name": "createdAt",
"type": "timestr",
"value": "6 tahun yang lalu"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@hizkiakurniawan",
"name": "username",
"type": "string",
"value": "@hizkiakurniawan"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@hizkiakurniawan",
"name": "content",
"type": "string",
"value": "Prabowo: saya, saya, saya, saya\nJokowi: kita, kita, kita, kita\nKeliatan yang mana yang mikirin diri sendiri"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@hizkiakurniawan",
"name": "likeCount",
"type": "number",
"value": "2500"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@hizkiakurniawan",
"name": "commentCount",
"type": "number",
"value": "52"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@hizkiakurniawan",
"name": "createdAt",
"type": "timestr",
"value": "6 tahun yang lalu"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@chintyaayumustika8842",
"name": "username",
"type": "string",
"value": "@chintyaayumustika8842"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@chintyaayumustika8842",
"name": "content",
"type": "string",
"value": "rindu debat ini , mereka berdua saat ini terbukti orang yang baik , sehat selalu pak jokowi dan pak prabowo"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@chintyaayumustika8842",
"name": "likeCount",
"type": "number",
"value": "75"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@chintyaayumustika8842",
"name": "commentCount",
"type": "number",
"value": "6"
},
{
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@chintyaayumustika8842",
"name": "createdAt",
"type": "timestr",
"value": "1 tahun yang lalu"
}
],
"savedAt": "2025-07-22T12:57:02.571Z",
"type": "comment",
"url": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV"
}
]

File diff suppressed because it is too large Load Diff

1
index.ts Normal file
View File

@@ -0,0 +1 @@
console.log("Hello via Bun!");

1797
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

21
package.json Normal file
View File

@@ -0,0 +1,21 @@
{
"name": "new-browser-input",
"module": "index.ts",
"type": "module",
"private": true,
"devDependencies": {
"@types/bun": "latest",
"@types/chrome": "^0.0.326",
"@typescript-eslint/eslint-plugin": "^8.34.0",
"@typescript-eslint/parser": "^8.34.0",
"eslint": "^9.28.0"
},
"peerDependencies": {
"typescript": "^5.8.3"
},
"dependencies": {
"@types/turndown": "^5.0.5",
"turndown": "^7.2.0",
"turndown-plugin-gfm": "^1.0.2"
}
}

BIN
public/extension.zip Normal file

Binary file not shown.

51
resultFacebook.js Normal file
View File

@@ -0,0 +1,51 @@
[
{
"facts": [
{
"context": "https://www.facebook.com",
"name": "fb_post_source_url",
"type": "url",
"value": "https://www.facebook.com"
},
{
"context": "https://www.facebook.com",
"name": "fb_post_profile_url",
"type": "url",
"value": "https://www.facebook.com/KOMPAScom"
},
{
"context": "https://www.facebook.com",
"name": "fb_post_display_name",
"type": "name",
"value": "Kompas.com"
},
{
"context": "https://www.facebook.com",
"name": "fb_post_content_md",
"type": "md",
"value": "[photo](https://scontent.fcgk18-1.fna.fbcdn.net/v/t39.30808-6/552480808_1307882211367890_924495273445433584_n.jpg?stp=dst-jpg_p526x296_tt6&_nc_cat=111&ccb=1-7&_nc_sid=127cfc&_nc_ohc=rmt_4eIvftYQ7kNvwGDecy3&_nc_oc=Adm4VTjN2xcbzrjqarwQfh5wArl6OaDDOgge44bwbwu0mLE98HEo_LPjTey2sV0LdR0&_nc_zt=23&_nc_ht=scontent.fcgk18-1.fna&_nc_gid=pbxoFcISGk0l9vQ7hd4buA&oh=00_AfYyGjiCbgz-zfzTKp3PpfjEbbNNpJuy-9OaKb9v4juRdA&oe=68DC6CFD)\n\nPresiden RI Prabowo Subianto menyatakan Indonesia akan mengakui Israel bila Israel mengakui Palestina.\n\nBaca selengkapnya ![👇](https://static.xx.fbcdn.net/images/emoji.php/v9/t4f/1/16/1f447.png)\n\n[https://nasional.kompas.com/.../prabowo-di-ktt-pbb-jika...](https://l.facebook.com/l.php?u=https%3A%2F%2Fnasional.kompas.com%2Fread%2F2025%2F09%2F23%2F05583471%2Fprabowo-di-ktt-pbb-jika-israel-akui-palestina-ri-akui-israel%3Ffbclid%3DIwZXh0bgNhZW0CMTAAYnJpZBExMGVEc0Y0eGxpdGdFcE5IQwEeY5f-Tnn1TkSGDkCAf9jxR_RaW2RZ_3eGhCiWBqDNMh9tPrSqhpvkaFhZioE_aem_8X5jz7VulHNDODRDBDC2cg&h=AT2u2bZowgyWIp1xOwLXROAsM_lcjBzkuGfLKKW6LCSoV-IOzOX8FSJGw96PcCaAG-YS4DP56vMdbVPzT7eiO8XcZO3kYYZ3Ewhz14uNm1e1DGm-CPAUaufbKpdsY4nXFIz3fBvl&__tn__=-UK-R&c[0]=AT3_uBBuaqdxsDtcTjjYawf8809vYPadik6-z9Yzi-N6-vQk7SiN8BHlTARL6iWFy1OHDAKqF9be7khVqDBUwjBLM8-4Jzxffgp5KVuzioPJZNRS1423JuqTb1zFMdzZFXhh4paVd8ZdppQpMOusBoNJSqa5SmxwZlTo99n9lnWaY2CdW6WYnIgXLWcr3AGS1tQOmNn6OqprGAu8p4Cg08s86w)\n\n~LL [#Palestina](https://www.facebook.com/hashtag/palestina?__cft__[0]=AZVIekghHa8vSfH_P_pyOdhfBlcsgze3ax0vSHyQF_kQfC4UMzScNJ0un_oGTcS2oT8CUCeiKEQNURVZW_ZdWdErLCTqfK1g-dziSlyk2J1mLbZsaFKZvbto7CADJCqITYqSZ4MHoa6fFH_PCYQ2NbJ1nYY1V5iIepUval71zIAiPyptkTlNxErtcxGAaMpQNaI&__tn__=*NK-R) [#Prabowo](https://www.facebook.com/hashtag/prabowo?__cft__[0]=AZVIekghHa8vSfH_P_pyOdhfBlcsgze3ax0vSHyQF_kQfC4UMzScNJ0un_oGTcS2oT8CUCeiKEQNURVZW_ZdWdErLCTqfK1g-dziSlyk2J1mLbZsaFKZvbto7CADJCqITYqSZ4MHoa6fFH_PCYQ2NbJ1nYY1V5iIepUval71zIAiPyptkTlNxErtcxGAaMpQNaI&__tn__=*NK-R)"
},
{
"context": "https://www.facebook.com",
"name": "fb_post_like_count",
"type": "integer",
"value": "15000"
},
{
"context": "https://www.facebook.com",
"name": "fb_post_comment_count",
"type": "integer",
"value": "7500"
},
{
"context": "https://www.facebook.com",
"name": "fb_post_share_count",
"type": "integer",
"value": "2000"
}
],
"savedAt": "2025-09-26T15:01:59.619Z",
"type": "post",
"url": "https://www.facebook.com/search/top?q=prabowo"
}
]

128
src/background.ts Normal file
View File

@@ -0,0 +1,128 @@
chrome.runtime.onInstalled.addListener(() => {
// You can perform any setup tasks here, like creating default settings
}
);
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === 'saveData') {
interface SaveDataItem {
type: string;
url: string;
[key: string]: any;
}
const dataArray: SaveDataItem[] = request.data as SaveDataItem[];
console.log('Received data for saving:', dataArray);
dataArray.forEach((item: SaveDataItem) => {
sendDataToServer(item, item.type, item.url);
});
async function sendDataToServer(data: any, type: string, url: string) {
if (!data || !type) {
console.error('Invalid data or type for sending to server:', { data, type });
return;
}
var resolvedUrl = await resolveUrl(type, url);
console.log(`Sending data to ${resolvedUrl}:`);
const options = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json, application/problem+json'
},
body: JSON.stringify(data)
};
try {
console.log(`Sending data to ${resolvedUrl}:`);
const response = await fetch(resolvedUrl, options).then(res => {
if (!res.ok) {
throw new Error(`HTTP error! status: ${res.status}`);
}
return res;
});
const responseData = await response.json();
console.log(responseData);
} catch (error) {
console.error(error);
}
}
async function resolveUrl(type: string, platform: string): Promise<string> {
const platformMap: Record<string, string> = {
"facebook.com": 'fb',
"instagram.com": 'ig',
"youtube.com": 'yt',
"linkedin.com": 'li',
"x.com": 'x',
"tiktok.com": 'tt'
};
// Get apiURL from storage asynchronously
const apiUrl = await new Promise<string>((resolve) => {
chrome.storage.local.get('apiURL', (result) => {
const url = result.apiURL || 'https://dropper.nakhari.us/';
console.log('Retrieved apiURL from storage:', url);
resolve(url);
});
});
// clean https://instagram.com/p/DMVZrA6hg4s/
// https://instagram.com/
const cleanedUrl = platform.replace(/https?:\/\//, '').replace(/www\./, '').split('/')[0];
const baseUrls: Record<string, string> = {
profile: `${apiUrl}${platformMap[cleanedUrl]}/profile`,
post: `${apiUrl}${platformMap[cleanedUrl]}/post`,
comment: `${apiUrl}${platformMap[cleanedUrl]}/comment`,
};
console.log('Final URL:', baseUrls[type]);
return baseUrls[type];
}
// Save to chrome.storage.session (Chrome 102+)
if (chrome.storage && chrome.storage.session) {
const timestamp = new Date().toISOString();
const key = `session_data_${timestamp}`;
chrome.storage.session.set({ [key]: request.data }, () => {
if (chrome.runtime.lastError) {
console.error('Error saving to session storage:', chrome.runtime.lastError);
sendResponse({ success: false, error: chrome.runtime.lastError.message });
} else {
// Kirim data kembali untuk ditampilkan otomatis
sendResponse({ success: true, key: key, data: request.data });
}
});
} else {
// Fallback ke local storage jika session storage tidak tersedia
console.warn('Session storage not available, using local storage');
const timestamp = new Date().toISOString();
const key = `local_data_${timestamp}`;
chrome.storage.local.set({ [key]: request.data }, () => {
if (chrome.runtime.lastError) {
sendResponse({ success: false, error: chrome.runtime.lastError.message });
} else {
// Kirim data kembali untuk ditampilkan otomatis
sendResponse({ success: true, key: key, data: request.data });
}
});
}
return true; // Important for async response
}
});

259
src/contentScript.ts Normal file
View File

@@ -0,0 +1,259 @@
import XModule from './modules/x';
import TiktokModule from './modules/tiktok';
import LinkedInModule from './modules/linkedin';
import YTModule from './modules/youtube';
import FacebookModule from './modules/facebook';
import InstagramModule from './modules/instagram';
function getActiveModule() {
const hostname = window.location.hostname;
if (hostname.includes('tiktok.com')) return new TiktokModule();
if (hostname.includes('x.com')) return new XModule();
if (hostname.includes('linkedin.com')) return new LinkedInModule();
if (hostname.includes('youtube.com')) return new YTModule();
if (hostname.includes('facebook.com')) return new FacebookModule();
if (hostname.includes('instagram.com')) return new InstagramModule();
return null;
}
function createToggleButton(): void {
// Hindari duplikat
if (document.getElementById('my-ext-toggle-btn')) return;
const btn = document.createElement('button');
btn.id = 'my-ext-toggle-btn';
btn.textContent = '<';
btn.style.cssText = `
position: fixed;
top: 100px;
right: 0;
z-index: 2147483646;;
width: 40px;
height: 40px;
background: #333;
color: #fff;
border: none;
border-radius: 4px;
font-size: 18px;
cursor: pointer;
`;
btn.addEventListener('click', () => {
const sidebar = document.getElementById('my-extension-sidepanel');
if (sidebar) {
sidebar.remove();
} else {
createSidebarPanel();
}
});
document.body.appendChild(btn);
}
const activeModule = getActiveModule();
// Track URL changes for SPA navigation
let currentUrl = window.location.href;
function reloadSidebarOnUrlChange(): void {
// Check for URL changes every 500ms
setInterval(() => {
if (window.location.href !== currentUrl) {
currentUrl = window.location.href;
console.log('URL changed, reloading sidebar...');
// Remove existing sidebar
const existingSidebar = document.getElementById('my-extension-sidepanel');
if (existingSidebar) {
existingSidebar.remove();
}
// Create new sidebar after a small delay to ensure page content is loaded
setTimeout(() => {
createSidebarPanel();
}, 500);
}
}, 500);
}
// Also listen for browser navigation events
// window.addEventListener('popstate', () => {
// console.log('Browser navigation detected, reloading sidebar...');
// setTimeout(() => {
// const existingSidebar = document.getElementById('my-extension-sidepanel');
// if (existingSidebar) {
// existingSidebar.remove();
// }
// createSidebarPanel();
// }, 500);
// });
// Listen for pushstate/replacestate (used by SPAs)
const originalPushState = history.pushState;
const originalReplaceState = history.replaceState;
// history.pushState = function (...args) {
// originalPushState.apply(history, args);
// setTimeout(() => {
// console.log('SPA navigation detected (pushState), reloading sidebar...');
// const existingSidebar = document.getElementById('my-extension-sidepanel');
// if (existingSidebar) {
// existingSidebar.remove();
// }
// createSidebarPanel();
// }, 500);
// };
// history.replaceState = function (...args) {
// originalReplaceState.apply(history, args);
// setTimeout(() => {
// console.log('SPA navigation detected (replaceState), reloading sidebar...');
// const existingSidebar = document.getElementById('my-extension-sidepanel');
// if (existingSidebar) {
// existingSidebar.remove();
// }
// createSidebarPanel();
// }, 500);
// };
function createSidebarPanel(): void {
// Remove existing sidebar if any
const existingSidebar = document.getElementById('my-extension-sidepanel');
if (existingSidebar) {
existingSidebar.remove();
}
const iframe = document.createElement('iframe');
iframe.id = 'my-extension-sidepanel';
iframe.src = chrome.runtime.getURL('popup/popup.html');
iframe.style.position = 'fixed';
iframe.style.top = '0';
iframe.style.right = '0';
iframe.style.width = '320px';
iframe.style.minWidth = '320px';
iframe.style.maxWidth = '320px';
iframe.style.height = '100vh';
iframe.style.zIndex = '2147483647';
iframe.style.border = 'none';
iframe.style.background = 'transparent';
iframe.style.boxShadow = '0 0 8px rgba(0,0,0,0.15)';
iframe.style.display = 'block';
document.body.appendChild(iframe);
console.log('Sidebar panel created for:', window.location.href);
}
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (!activeModule) {
console.warn('No active module found for this site');
sendResponse({ success: false, error: 'No active module found' });
return;
}
switch (request.action) {
case 'toggleSidebar':
const sidebar = document.getElementById('my-extension-sidepanel');
if (sidebar) {
sidebar.remove();
} else {
createSidebarPanel();
}
sendResponse({ success: true });
break;
case 'tagElement':
activeModule.tagElement();
sendResponse({ success: true });
console.log(resolveUrl('profile'));
break;
case 'saveData':
// Handle async saveData for TikTok
(async () => {
try {
const data = await activeModule.saveData();
// data.forEach(item => {
// sendDataToServer(item, item.type);
// })
// Send to background for saving
chrome.runtime.sendMessage({ action: 'saveData', data }, (response) => {
if (response?.success) {
// Send data back to popup for automatic display
sendResponse({ success: true, data: response.data, key: response.key, saved: true });
} else {
sendResponse({ success: false, error: 'Failed to save data' });
}
});
} catch (error) {
console.error('Error in saveData:', error);
const errorMessage = error instanceof Error ? error.message : String(error);
sendResponse({ success: false, error: `Failed to fetch data: ${errorMessage}` });
}
})();
return true; // Important: Indicates that the response is sent asynchronously
default:
sendResponse({ success: false, error: 'Unknown action' });
}
});
async function sendDataToServer(data: any, type: string) {
if (!data || !type) {
console.error('Invalid data or type for sending to server:', { data, type });
return;
}
var url = resolveUrl(type);
const options = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json, application/problem+json'
},
body: JSON.stringify(data)
};
try {
const response = await fetch(url, options);
const data = await response.json();
console.log(data);
} catch (error) {
console.error(error);
}
}
function resolveUrl(type: string): string {
const platformMap: Record<string, string> = {
"www.facebook.com": 'fb',
"www.instagram.com": 'ig',
"www.youtube.com": 'yt',
"www.linkedin.com": 'li',
"x.com": 'x',
"www.tiktok.com": 'tt'
};
const apiUrl = 'http://192.168.10.176:8888/'
const baseUrls: Record<string, string> = {
profile: `${apiUrl + platformMap[window.location.hostname]}/profile`,
post: `${apiUrl + platformMap[window.location.hostname]}/post`,
comment: `${apiUrl + platformMap[window.location.hostname]}/comment`,
};
return baseUrls[type];
}
// Initialize sidebar and start monitoring
createSidebarPanel();
createToggleButton();
// reloadSidebarOnUrlChange();

61
src/helper/parseCount.ts Normal file
View File

@@ -0,0 +1,61 @@
export function parseCount(value: string): string {
if (!value || value.trim() === "") return "0";
let s = value.toLowerCase().trim();
// --- 1) Deteksi multiplier (satuan)
const units: Record<string, number> = {
jt: 1_000_000, juta: 1_000_000, m: 1_000_000, mio: 1_000_000,
rb: 1_000, ribu: 1_000, k: 1_000,
b: 1_000_000_000, miliar: 1_000_000_000, milyar: 1_000_000_000,
};
let multiplier = 1;
for (const [key, mult] of Object.entries(units)) {
if (s.includes(key)) {
multiplier = mult;
// Hapus label satuan agar angka mudah diparse
s = s.replace(new RegExp(key, "g"), "");
}
}
// --- 2) Normalisasi pemisah ribuan/desimal
// Kasus A: punya titik & koma -> anggap '.' ribuan, ',' desimal => buang '.', ganti ',' -> '.'
if (s.includes(".") && s.includes(",")) {
s = s.replace(/\./g, "").replace(/,/g, ".");
} else {
// Kasus B: hanya koma
const hasOnlyComma = s.includes(",") && !s.includes(".");
// Jika ada pola ribuan '1,234' (barat), buang koma.
if (hasOnlyComma && /\b\d{1,3}(,\d{3})+\b/.test(s)) {
s = s.replace(/,/g, "");
} else if (hasOnlyComma) {
// Anggap koma = desimal (Indonesia)
s = s.replace(/,/g, ".");
}
// Kasus C: hanya titik -> cek apakah pola ribuan Indonesia '1.234' atau desimal
if (!s.includes(",") && s.includes(".")) {
if (/\b\d{1,3}(\.\d{3})+\b/.test(s)) {
// jelas ribuan -> hapus titik
s = s.replace(/\./g, "");
}
// else: biarkan '.' sebagai desimal
}
}
// --- 3) Tangani pola ribuan besar yang tersisa (jaga-jaga)
if (/\d+\.\d+\.\d+/.test(s)) {
s = s.replace(/[^\d]/g, ""); // ambil digit saja
}
// --- 4) Ambil angka pertama (boleh desimal tunggal)
const m = s.match(/(\d+(?:\.\d+)?)/);
if (!m) return "0";
const num = parseFloat(m[1]);
if (Number.isNaN(num)) return "0";
// --- 5) Hasil akhir
const out = Math.round(num * multiplier);
return out.toString();
}

10
src/index.html Normal file
View File

@@ -0,0 +1,10 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<video class="x1lliihq x5yr21d xh8yej3" playsinline="" preload="none" src="blob:https://www.instagram.com/0211cd5e-7ae3-419f-8cdc-bcef367d3007" style="display: block;"></video>
</body>
</html>

63
src/interfaces.ts Normal file
View File

@@ -0,0 +1,63 @@
interface PostResult {
ulid?: string;
url: string;
post: Post ;
uploadedAt: string;
comments: Comment[];
}
interface Post {
ulid?:string;
url:string
username?: string;
displayName: string;
content: string;
type: string;
likeCount:number;
shareCount:number;
commentCount:number;
viewCount?:number; //tiktok, x
bookmarkCount?:number;
savedAt?: string; // if format "6h ago"
date?: string;
}
interface Comment{
ulid?: string;
url?:string;
username?: string;
postUrl:string;
displayName: string;
content: string;
type: string;
likeCount:number;
shareCount?:number;
commentCount:number;
savedAt?: string;
}
interface ProfileResult {
ulid?:string;
url: string;
type: string;
profile: Profile;
uploadedAt: string;
posts: Post[];
}
interface Profile {
ulid?:string;
username?: string;
displayName: string;
description: string;
image: string;
createdAt?: string; // x only
followingCount:number;
followerCount:number;
postCount?:number;
likeCount?:number;
}
export type { PostResult, Post, Comment, ProfileResult, Profile };

9
src/mark.css Normal file
View File

@@ -0,0 +1,9 @@
[class*="tagged"]
{
border: 2px solid #ff0000 !important;
}
[class*="saved"]
{
border: 2px solid #00ff00 !important;
}

60
src/model.ts Normal file
View File

@@ -0,0 +1,60 @@
interface Fact {
context?: string; // url of the post or profile, optional
name: string;
type: 'string' | 'number' | 'url' | 'boolean' | 'datetime' | 'timestr' | 'name' | 'md' | 'text' | 'integer';
value: string | number | boolean | object;
}
interface FactCollection {
url: string;
facts: Fact[];
savedAt: string; // time.now()
type: 'profile' | 'post' | 'search' |'comment';
}
/**
*
* @param name property name
* @param type property type
* @param value property value
* @param context optional context, usually the url of the post or profile
* @returns
*/
export function NewFact( name: string, type:'string' | 'number' | 'url' | 'boolean' | 'datetime' | 'timestr' | 'name' | 'md' | 'text' | 'integer', value:string, context?:string):Fact {
const fact:Fact = {
context: context,
name: name,
type:type,
value: value,
}
return fact;
}
/**
*
* @param contextp url of the post or profile
* @param facts array of facts
* @param type type of the fact collection, can be 'profile', 'post', or 'comment'
* @returns
*/
export function NewFactCollection(type: 'profile' | 'post' | 'comment' | 'search'): FactCollection {
const factCollection: FactCollection = {
url: window.location.href, // use window location href if url is not provided
facts: [],
savedAt: new Date().toISOString(),
type: type,
}
return factCollection;
}
export type{
Fact,
FactCollection,
};

1025
src/modules/facebook.ts Normal file

File diff suppressed because it is too large Load Diff

763
src/modules/instagram.ts Normal file
View File

@@ -0,0 +1,763 @@
import * as model from '../model';
import { turndownService } from '../utils/turndown';
import { parseCount } from '../helper/parseCount';
const SELECTORS = {
profile: {
container: 'main header',
avatar: 'img[alt*="profile picture" i], img[alt*="profile" i], header img[alt]',
displayName: 'header section:nth-child(4) span[dir="auto"]',
verifiedBadge: 'svg[aria-label="Verified"], img[alt="Verified"]',
bio: 'main header section:nth-child(4) > div > span',
followers: 'a[href*="/followers/"] span',
following: 'a[href*="/following/"] span',
},
post: {
container: 'article',
username: 'a[href*="/"][href$="/"]',
caption: 'h1[dir="auto"]',
likeCount: 'a[href*="/liked_by/"] span[class*="html-span"]',
// media: 'video[src], img[src*="instagram"]',
mediaContainer: 'div[role="presentation"]',
videoUrl: 'article > div > div:first-child video[src], article > div > div:first-child video[src] source',
imageUrl: 'article > div > div:first-child img[src]',
timestamp: 'time[datetime]',
profileImage: 'img[alt*="profile"]',
profileUrl: 'a[href*="/"][href$="/"]'
},
comments: {
container: 'article > div > div:nth-child(2) > div > div > div:nth-child(2) > div > ul > div:nth-child(3) > div > div, main[role="main"] hr + div > div > div:nth-child(2)',
sections: ':scope > div',
profile_url: 'a[href*="/"]',
user_name: 'a[href*="/"] span[dir="auto"]',
createdat_datetime: 'time[datetime]',
profileimage_url: 'img[alt*="profile"]',
comment_url: 'a[href*="/p/"]',
like_count: 'div[role="button"] > span[dir="auto"], span[dir="ltr"] button > span',
replie_count: 'div[role="button"] > div[class^="html-div"] > span[dir="auto"], li ul li button > span',
content_md: 'div[class^="html-div"] > div > div[class^="html-div"] > div[class^="html-div"] > div[class^="html-div"] > div > div[class^="html-div"] > div:nth-of-type(2)',
username: 'a[href*="/"][href$="/"]',
text: 'span[dir="auto"]',
timestamp: 'time[datetime]',
profileImage: 'img[alt*="profile"]'
}
};
export default class InstagramModule {
async saveData(): Promise<model.FactCollection[]> {
const collections: model.FactCollection[] = [];
try {
if (this.isProfilePage()) {
const profile = this.extractProfile();
if (profile) collections.push(profile);
const posts = this.extractPostsList();
if (posts) collections.push(posts);
} else if (this.isPostPage()) {
const post = this.extractDetailPost();
if (post) collections.push(post);
const comments = this.extractComments();
if (comments) collections.push(comments);
}
else if(window.location.href.includes('/explore/') || window.location.href.includes('/search/')){
const posts = this.extractPostsList();
if (posts?.facts && posts.facts.length > 0) collections.push(posts);
}
} catch (error) {
console.error("Error saving Instagram data:", error);
}
return collections;
}
private isProfilePage(): boolean {
return !window.location.pathname.includes('/p/') && !window.location.pathname.includes('/reel/') && !window.location.href.includes('/search/');
}
private isPostPage(): boolean {
return window.location.pathname.includes('/p/') || window.location.pathname.includes('/reel/');
}
private extractProfile(): model.FactCollection | null {
if (this.isSaved(document.querySelector(SELECTORS.profile.container) as HTMLElement)
) {
return null;
}
const profile = model.NewFactCollection('profile');
profile.url = this.getCleanURL();
if (profile.url) profile.facts.push(model.NewFact("ig_profile_source_url", "url", profile.url, window.location.href));
const displayName = document.querySelector(SELECTORS.profile.displayName)?.textContent?.trim();
if (displayName) profile.facts.push(model.NewFact("ig_profile_display_name", "name", displayName, window.location.href));
// Username from URL
const username = this.getUsername();
if (username) profile.facts.push(model.NewFact("ig_profile_user_name", "name", username, window.location.href));
// Avatar
const avatar = document.querySelector(SELECTORS.profile.avatar) as HTMLImageElement;
if (avatar?.src) profile.facts.push(model.NewFact("ig_profile_profileimage_url", "url", avatar.src, window.location.href));
// Bio
const bioEl = document.querySelector(SELECTORS.profile.bio) as HTMLElement;
const bioLinkEl = document.querySelector('main header section:nth-child(4) > div > a') as HTMLAnchorElement;
const bioTurndown = turndownService.turndown(bioEl?.innerHTML?.trim() || '');
if (bioEl) {
profile.facts.push(model.NewFact("ig_profile_bio_text", "text", bioTurndown
, window.location.href));
}
// Stats from meta description
const stats = this.extractStatsFromMeta();
Object.entries(stats).forEach(([key, value]) => {
if (value) profile.facts.push(model.NewFact(key, "integer", String(this.parseNumber(value)), window.location.href));
});
if (!stats.followerCount) {
// Fallback to followers count from profile page
const followersEl = document.querySelector(SELECTORS.profile.followers) as HTMLElement;
if (followersEl) {
const followerCount = followersEl.textContent?.trim();
const cleanedfollowerCount = parseCount(followerCount.split(' ')[0]);
if (followerCount) {
profile.facts.push(model.NewFact("ig_profile_follower_count", "integer", cleanedfollowerCount, window.location.href));
}
}
}
if (!stats.followerCount) {
// Fallback to following count from profile page
const followingEl = document.querySelector(SELECTORS.profile.following) as HTMLElement;
if (followingEl) {
const followingCount = followingEl.textContent?.trim();
const cleanedFollowingCount = parseCount(followingCount.split(' ')[0]);
if (followingCount) {
profile.facts.push(model.NewFact("ig_profile_following_count", "integer", cleanedFollowingCount, window.location.href));
}
}
}
if (profile.facts.length > 0) {
this.addClassSaved(document.querySelector(SELECTORS.profile.container) as HTMLElement);
return profile;
} else {
return null;
}
}
private extractPostsList(): model.FactCollection | null {
const posts = window.location.href.includes('/search/') ? model.NewFactCollection('search') : model.NewFactCollection('post');
posts.url = this.getCleanURL();
const postLinks = document.querySelectorAll<HTMLAnchorElement>('a[href*="/p/"]:not(.saved), a[href*="/reel/"]:not(.saved)');
postLinks.forEach(link => {
posts.facts.push(model.NewFact("ig_post_source_url", "url", link.href, link.href));
// const img = link.querySelector('img') as HTMLImageElement;
// if (img?.src) {
// posts.facts.push(model.NewFact("thumbnail_url", "url", img.src, link.href));
// if (img.alt) posts.facts.push(model.NewFact("description", "string", img.alt, link.href));
// }
// Tandai sebagai tersimpan
this.addClassSaved(link);
});
return posts.facts.length > 0 ? posts : null;
}
private extractFromJSON(): model.FactCollection | null {
const scripts = document.querySelectorAll('script[type="application/json"]:not(.saved)');
const postId = this.getPostId();
for (const script of scripts) {
const content = script.textContent;
if (!content || !content.includes('like_count') || !content.includes(postId)) continue;
try {
const data = JSON.parse(content);
const postData = this.findPostInJSON(data);
if (!postData) continue;
console.log('📦 Found post data structure:', {
hasCarousel: !!postData.carousel_media,
mediaType: postData.media_type,
productType: postData.product_type,
carouselCount: postData.carousel_media?.length || 0,
hasUser: !!(postData.user || postData.owner),
username: (postData.user || postData.owner)?.username
});
const post = model.NewFactCollection('post');
post.url = this.getCleanURL();
// Basic fields
post.facts.push(model.NewFact("ig_post_source_url", "url", post.url, window.location.href));
// Extract user information
const userInfo = postData.user || postData.owner;
if (userInfo) {
console.log('👤 Found user info:', {
username: userInfo.username,
hasProfilePic: !!userInfo.profile_pic_url,
hasHDProfilePic: !!userInfo.hd_profile_pic_url_info?.url
});
if (userInfo.username) {
post.facts.push(model.NewFact("ig_post_user_name", "name", userInfo.username, window.location.href));
// Generate profile URL from username
const profileUrl = `https://www.instagram.com/${userInfo.username}/`;
post.facts.push(model.NewFact("ig_post_profile_url", "url", profileUrl, window.location.href));
console.log('✅ Added username and profile URL');
}
// Extract profile image
if (userInfo.profile_pic_url) {
post.facts.push(model.NewFact("ig_post_profileimage_url", "url", userInfo.profile_pic_url, window.location.href));
console.log('✅ Added profile image from profile_pic_url');
} else if (userInfo.hd_profile_pic_url_info?.url) {
post.facts.push(model.NewFact("ig_post_profileimage_url", "url", userInfo.hd_profile_pic_url_info.url));
console.log('✅ Added profile image from hd_profile_pic_url_info');
}
} else {
console.log('⚠️ No user info found in post data');
}
// Extract media content (images/videos) and caption
const mediaContent = this.extractMediaFromJSON(postData, data);
if (mediaContent.content) {
post.facts.push(model.NewFact("ig_post_content_md", "md", mediaContent.content, window.location.href));
console.log('✅ Extracted media content successfully');
}
if (postData.like_count > 0) {
post.facts.push(model.NewFact("ig_post_like_count", "integer", String(postData.like_count), window.location.href));
}
if (postData.comment_count >= 0) {
post.facts.push(model.NewFact("ig_post_comment_count", "integer", String(postData.comment_count), window.location.href));
}
// // Legacy single media URL
// if (postData.display_url) {
// post.facts.push(model.NewFact("ig_post_media_url", "url", postData.display_url, window.location.href));
// }
const date = document.querySelector('time[datetime]');
if (date) {
const datetime = date.getAttribute('datetime');
if (datetime) {
post.facts.push(model.NewFact("ig_post_createdat_datetime", "datetime", datetime, window.location.href));
}
}
this.addClassSaved(script as HTMLElement);
return post.facts.length > 0 ? post : null;
} catch (error) {
console.error('❌ Error parsing JSON:', error);
continue;
}
}
return null;
}
private extractDetailPost(): model.FactCollection | null {
const post = model.NewFactCollection('post');
post.url = this.getCleanURL();
// Try JSON first, then DOM
const jsonPost = this.extractFromJSON();
if (jsonPost) {
return jsonPost;
}
// Basic info
post.facts.push(model.NewFact("ig_post_source_url", "url", window.location.href));
const container = document.querySelector(SELECTORS.post.container);
if (!container || container.classList.contains('saved')) return null;
// Username
const usernameEl = this.findElement(SELECTORS.post.username, container);
if (usernameEl) {
const username = this.extractUsernameFromHref(usernameEl.getAttribute('href'));
if (username) post.facts.push(model.NewFact("ig_post_user_name", "name", username, window.location.href));
}
// Profile URL
const profileUrlEl = this.findElement(SELECTORS.post.profileUrl, container);
if (profileUrlEl) {
const profileUrl = profileUrlEl.getAttribute('href');
const fullProfileUrl = profileUrl?.includes('instagram.com') ? profileUrl : `https://www.instagram.com${profileUrl}`;
if (profileUrl) post.facts.push(model.NewFact("ig_post_profile_url", "url", fullProfileUrl, window.location.href));
}
// Avatar
const profileImage = this.findElement(SELECTORS.post.profileImage, container) as HTMLImageElement;
if (profileImage?.src) {
post.facts.push(model.NewFact("ig_post_profileimage_url", "url", profileImage.src, window.location.href));
}
// Content of post
const caption = this.findElement(SELECTORS.post.caption, container)?.textContent?.trim();
const mediaContainer = this.findElement(SELECTORS.post.mediaContainer, container);
const imageUrl = mediaContainer?.querySelectorAll(SELECTORS.post.imageUrl) as NodeListOf<HTMLImageElement>;
const imageUrls: string[] = [...imageUrl].map(img => img.src).filter(src => src);
const videoUrl = mediaContainer?.querySelectorAll(SELECTORS.post.videoUrl) as NodeListOf<HTMLVideoElement>;
const videoUrls: string[] = [...videoUrl].map(video => video.src).filter(src => src);
const contentParts: string[] = [];
// Add images
if (imageUrls.length > 0) {
const imageMarkdown = imageUrls.map(url => `![image](${url})`).join('\n');
contentParts.push(imageMarkdown);
}
// Add video
if (videoUrls.length > 0) {
const videoMarkdown = videoUrls.map(url => `![video](${url})`).join('\n');
contentParts.push(videoMarkdown);
}
// Add caption
if (caption) {
contentParts.push(caption);
}
// Join dengan double newline untuk proper spacing
const contentTurndown = contentParts.join('\n\n');
post.facts.push(model.NewFact("ig_content_md", "md", contentTurndown, window.location.href));
// Likes
const likesEl = this.findElement(SELECTORS.post.likeCount, container);
if (likesEl?.textContent) {
const likes = this.parseNumber(likesEl.textContent);
if (likes > 0) post.facts.push(model.NewFact("ig_post_like_count", "integer", String(likes), window.location.href));
}
// Timestamp
const timeEl = this.findElement(SELECTORS.post.timestamp, container);
if (timeEl) {
const datetime = timeEl.getAttribute('datetime');
if (datetime) post.facts.push(model.NewFact("ig_post_createdat_datetime", "datetime", datetime, window.location.href));
}
// Tandai container sebagai tersimpan
if (post.facts.length > 0) {
this.addClassSaved(container as HTMLElement);
}
return post.facts.length > 0 ? post : null;
}
// Extract post data from JSON embedded in the page (example: ../../htmlexample/instagram/detailPost.json)
private findPostInJSON(obj: any): any {
if (!obj || typeof obj !== 'object') return null;
// Check if current object is a valid post
if (obj.like_count !== undefined && (obj.user || obj.owner)) {
return obj;
}
// Check for carousel container (product_type: "carousel_container")
if (obj.product_type === 'carousel_container' && obj.carousel_media) {
return obj;
}
// Search in common paths
const paths = [
'graphql.shortcode_media',
'media',
'__bbox.result',
'xdt_api__v1__media__shortcode__web_info.items.0', // New Instagram API format
'require.0.3.0.__bbox.require.0.3.1.__bbox.result.data.xdt_api__v1__media__shortcode__web_info.items.0'
];
for (const path of paths) {
const result = this.getNestedValue(obj, path);
if (result && (result.like_count !== undefined || result.carousel_media)) {
return result;
}
}
// Special handling for nested carousel structure
if (obj.items && Array.isArray(obj.items) && obj.items.length > 0) {
const firstItem = obj.items[0];
if (firstItem.carousel_media || firstItem.like_count !== undefined) {
return firstItem;
}
}
// Recursive search with depth limit to avoid infinite loops
return this.findPostInJSONRecursive(obj, 0, 5);
}
private findPostInJSONRecursive(obj: any, depth: number, maxDepth: number): any {
if (depth >= maxDepth || !obj || typeof obj !== 'object') return null;
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
const value = obj[key];
// Check current value
if (value && typeof value === 'object') {
if (value.like_count !== undefined && (value.user || value.owner)) {
return value;
}
if (value.product_type === 'carousel_container' && value.carousel_media) {
return value;
}
// Recursive search
const result = this.findPostInJSONRecursive(value, depth + 1, maxDepth);
if (result) return result;
}
}
}
return null;
}
private extractMediaFromJSON(postData: any, fullData: any): { content: string } {
const contentParts: string[] = [];
const imageUrls: string[] = [];
const videoUrls: string[] = [];
console.log('🔍 Extracting media from post data...');
// Check if it's a carousel post
if (postData.carousel_media && Array.isArray(postData.carousel_media)) {
console.log(`📸 Found carousel with ${postData.carousel_media.length} items`);
// Extract media from carousel
for (const mediaItem of postData.carousel_media) {
if (mediaItem.media_type === 2 && mediaItem.video_versions) {
// Video media
const videoUrl = this.getBestVideoURL(mediaItem.video_versions);
if (videoUrl) {
videoUrls.push(videoUrl);
console.log('🎥 Added video URL from carousel');
}
} else if (mediaItem.media_type === 1 && mediaItem.image_versions2?.candidates) {
// Image media
const imageUrl = this.getBestImageURL(mediaItem.image_versions2.candidates);
if (imageUrl) {
imageUrls.push(imageUrl);
console.log('📷 Added image URL from carousel');
}
}
}
} else {
console.log('📱 Processing single media post');
// Single media post
if (postData.media_type === 2 && postData.video_versions) {
// Single video
const videoUrl = this.getBestVideoURL(postData.video_versions);
if (videoUrl) {
videoUrls.push(videoUrl);
// console.log('🎥 Added single video URL');
}
} else if (postData.media_type === 1 && postData.image_versions2?.candidates) {
// Single image
const imageUrl = this.getBestImageURL(postData.image_versions2.candidates);
if (imageUrl) {
imageUrls.push(imageUrl);
// console.log('📷 Added single image URL');
}
}
}
// Check for video_dash_prefetch_representations (from multiplepost.json structure)
const extensions = this.findExtensionsInJSON(fullData);
if (extensions?.all_video_dash_prefetch_representations) {
console.log(`🎬 Found ${extensions.all_video_dash_prefetch_representations.length} video dash representations`);
for (const videoData of extensions.all_video_dash_prefetch_representations) {
if (videoData.representations) {
for (const rep of videoData.representations) {
if (rep.mime_type === 'video/mp4' && rep.base_url) {
videoUrls.push(rep.base_url);
// console.log('🎥 Added video URL from dash representation');
break; // Take first video URL found per video
}
}
}
}
}
console.log(`📊 Media extraction result: ${imageUrls.length} images, ${videoUrls.length} videos`);
// Build content in Markdown format
if (imageUrls.length > 0) {
const imageMarkdown = imageUrls.map(url => `![image](${url})`).join('\n');
contentParts.push(imageMarkdown);
}
if (videoUrls.length > 0) {
const videoMarkdown = videoUrls.map(url => `![video](${url})`).join('\n');
contentParts.push(videoMarkdown);
}
// Add caption if exists
if (postData.caption?.text) {
contentParts.push(postData.caption.text);
console.log('📝 Added caption text');
}
return {
content: contentParts.join('\n\n')
};
}
private findExtensionsInJSON(obj: any): any {
if (!obj || typeof obj !== 'object') return null;
// Direct extensions property
if (obj.extensions) return obj.extensions;
// Search recursively with limited depth
const searchExtensions = (data: any, depth: number = 0): any => {
if (depth > 3 || !data || typeof data !== 'object') return null;
for (const key in data) {
if (data.hasOwnProperty(key)) {
if (key === 'extensions' && data[key]) return data[key];
if (typeof data[key] === 'object') {
const nested = searchExtensions(data[key], depth + 1);
if (nested) return nested;
}
}
}
return null;
};
return searchExtensions(obj);
}
private getBestVideoURL(videoVersions: any[]): string | null {
if (!videoVersions || videoVersions.length === 0) return null;
// Find highest quality video (type 101 is usually best quality)
const bestVideo = videoVersions.find(v => v.type === 101) || videoVersions[0];
return bestVideo?.url || null;
}
private getBestImageURL(candidates: any[]): string | null {
if (!candidates || candidates.length === 0) return null;
// Find highest resolution image
const validCandidates = candidates.filter(c => c.url && c.width && c.height);
if (validCandidates.length === 0) return null;
const bestImage = validCandidates.reduce((best, current) => {
const currentRes = current.width * current.height;
const bestRes = best.width * best.height;
return currentRes > bestRes ? current : best;
}, validCandidates[0]);
return bestImage?.url || null;
}
/**
* Extracts **all** top-level comments from an Instagram post
* using the exact DOM path proven in the JSDOM demo.
*/
private extractComments(): model.FactCollection | null {
const container = document.querySelector(
SELECTORS.comments.container
) as HTMLElement | null;
if (!container) return null;
/* 👉 grab every top-level comment block */
const sections = container.querySelectorAll<HTMLElement>(':scope > div');
if (sections.length === 0) return null;
const facts = model.NewFactCollection('comment');
sections.forEach(section => {
if (section.classList.contains('saved')) return;
const profile_url = section.querySelector(SELECTORS.comments.profile_url);
const user_name = section.querySelector(SELECTORS.comments.user_name);
const createdat_datetime = section.querySelector(SELECTORS.comments.createdat_datetime);
const profileimage_url = section.querySelector(SELECTORS.comments.profileimage_url);
const comment_url = section.querySelector(SELECTORS.comments.comment_url);
const like_count = section.querySelector(SELECTORS.comments.like_count);
const replie_count = section.querySelector(SELECTORS.comments.replie_count);
const content_md = section.querySelector(SELECTORS.comments.content_md);
const commentUrl = comment_url?.getAttribute('href');
const fullCommentUrl = commentUrl ? new URL(commentUrl, window.location.href).toString() : null;
if (fullCommentUrl) {
if (commentUrl) facts.facts.push(model.NewFact("ig_comment_source_url", "url", fullCommentUrl, window.location.href));
}
if (profile_url) {
const profileUrl = profile_url.getAttribute('href')
const fullProfileUrl = profileUrl?.includes('instagram.com') ? profileUrl : `https://www.instagram.com${profileUrl}`;
if (profileUrl) facts.facts.push(model.NewFact("ig_comment_profile_url", "name", fullProfileUrl, fullCommentUrl ?? window.location.href));
const username = this.extractUsernameFromHref(profile_url.getAttribute('href'));
if (username) facts.facts.push(model.NewFact("ig_comment_user_name", "name", username, fullCommentUrl ?? window.location.href));
}
if (content_md) {
const contentTurndown = turndownService.turndown(content_md.innerHTML);
if (contentTurndown) facts.facts.push(model.NewFact("ig_comment_content_md", "md", contentTurndown, fullCommentUrl ?? window.location.href));
}
if (profileimage_url) {
const imageUrl = (profileimage_url as HTMLImageElement).src;
if (imageUrl) facts.facts.push(model.NewFact("ig_comment_profileimage_url", "url", imageUrl, fullCommentUrl ?? window.location.href));
}
if (like_count) {
const likes = like_count.textContent?.trim() || '0';
if (likes) {
const likeMatch = likes.replaceAll(',', '').match(/\d+/g);
const likeCountStr = likeMatch ? likeMatch.join('') : '0';
facts.facts.push(model.NewFact("ig_comment_like_count", "integer", likeCountStr, fullCommentUrl ?? window.location.href));
}
}
if (replie_count) {
const replies = replie_count.textContent?.trim() || '0';
if (replies) {
const replyMatch = replies.replaceAll(',', '').match(/\d+/g);
const replyCountStr = replyMatch ? replyMatch.join('') : '0';
facts.facts.push(model.NewFact("ig_comment_comment_count", "integer", replyCountStr, fullCommentUrl ?? window.location.href));
}
}
if (createdat_datetime) {
const datetime = createdat_datetime.getAttribute('datetime');
if (datetime) facts.facts.push(model.NewFact("ig_comment_createdat_datetime", "datetime", datetime, fullCommentUrl ?? window.location.href));
}
this.addClassSaved(section as HTMLElement); // Mark section as saved
});
// container.classList.add('saved');
return facts.facts.length > 0 ? facts : null;
}
private getCleanURL(): string {
const url = new URL(window.location.href);
// Remove img_index parameter
url.searchParams.delete('img_index');
return url.toString();
}
private getUsername(): string | null {
const segments = window.location.pathname.split('/').filter(Boolean);
return segments[0] && segments[0].length < 50 ? segments[0] : null;
}
private extractStatsFromMeta(): Record<string, string> {
const ogDesc = document.querySelector('meta[property="og:description"]');
const content = ogDesc?.getAttribute('content') || '';
const stats: Record<string, string> = {};
const patterns = {
ig_profile_post_count: /(\d+[,\d]*)\s*Posts/i,
ig_profile_follower_count: /(\d+[,\d]*)\s*Followers/i,
ig_profile_following_count: /(\d+[,\d]*)\s*Following/i
};
Object.entries(patterns).forEach(([key, pattern]) => {
const match = content.match(pattern);
if (match) stats[key] = match[1];
});
return stats;
}
private extractUsernameFromHref(href: string | null): string | null {
if (!href) return null;
const match = href.match(/\/([^\/]+)\/$/);
return match && match[1] !== 'p' && match[1] !== 'reel' ? match[1] : null;
}
private findElement(selector: string, container?: Element | null): Element | null {
return container ? container.querySelector(selector) : document.querySelector(selector);
}
private parseNumber(text: string): number {
const clean = text.toLowerCase().replace(/[^0-9km.,]/g, '');
if (clean.includes('k')) return Math.round(parseFloat(clean.replace('k', '')) * 1000);
if (clean.includes('m')) return Math.round(parseFloat(clean.replace('m', '')) * 1000000);
return parseInt(clean.replace(/[,.]/g, '')) || 0;
}
private getNestedValue(obj: any, path: string): any {
return path.split('.').reduce((current, key) => current?.[key], obj);
}
private isTimeElement(text: string): boolean {
return /^\d+[dwmh]$/.test(text);
}
private getPostId(): string {
const match = window.location.pathname.match(/\/p\/([^/]+)/);
return match ? match[1] : '';
}
tagElement(): void {
const elements = document.querySelectorAll('article, img[alt*="profile"], a[href*="/p/"], a[href*="/reel/"]');
elements.forEach(el => el.classList.add('tagged'));
console.log(window.location.hostname);
}
addClassTagged(element: HTMLElement): void {
element.classList.add('tagged');
}
addClassSaved(element: HTMLElement): void {
element.classList.add('saved');
}
isTagged(element: HTMLElement): boolean {
return element.classList.contains('tagged');
}
isSaved(element: HTMLElement): boolean {
return element.classList.contains('saved');
}
}

629
src/modules/linkedin.ts Normal file
View File

@@ -0,0 +1,629 @@
import * as model from "../model";
import { NewFact } from "../model";
import { turndownService } from "../utils/turndown";
const ELEMENT_SELECTORS = {
profile: {
container: "#recent-activity-top-card",
displayName: "h1[class*='org-top-card-summary__title'], #recent-activity-top-card h3",
description: "#recent-activity-top-card h4",
image: "[class*='logo-container'] img, img.pv-recent-activity-top-card__member-photo",
followerCount: "li[class='text-body-small t-black--light inline-block'] > span, div.pv-recent-activity-top-card__extra-info > div > div:nth-child(2)",
},
post: {
container: "[data-view-name='feed-full-update']",
showMore: "button[class*='text__see-more'], button[role='button'][class*='see-more']",
url: "div.feed-shared-update-v2",
displayName: "span[class='update-components-actor__title'] span[dir='ltr'] > span > span, span[class='update-components-actor__title'] span[dir='ltr'] > span:first-of-type",
username: "span.feed-shared-actor__sub-description a",
profileImage: "div[class*='ivm-view-attr__img-wrapper'] img",
content: [
"div.feed-shared-inline-show-more-text",
"[class*='update-components-image__container-wrapper'] img",
"[class*='video-js media-player__player'] video",
"div.ivm-view-attr__blur-background img",
],
like: ["button[aria-label*='reactions'] span", "button.social-details-social-counts__count-value span"],
comment: "li[class*='social-details-social-counts__comments'] button",
share: "button[class*='social-details-social-counts__item--truncate-text'] > span",
date: '.update-components-actor__sub-description span[aria-hidden="true"]'
},
comment: {
container: "article.comments-comment-entity",
displayName: "span.comments-comment-meta__description-title",
urlProfile: "article.comments-comment-entity a",
profileImage: "div[class*='ivm-view-attr__img-wrapper'] img",
content: [
"div.update-components-text",
"[class*='update-components-image__container-wrapper'] img",
"[class*='video-js media-player__player'] video",
],
like: "button.comments-comment-social-bar__reactions-count--cr > span",
commentCount: "span.comments-comment-social-bar__replies-count--cr",
date: "time.comments-comment-meta__data"
}
} as const;
export default class LinkedInModule {
context: string = "";
followerCount: string = "";
private currentUrl: string = "";
constructor() {
this.currentUrl = window.location.href;
this.updateFollowerCount();
// Reset follower count when URL changes (different profile)
window.addEventListener('popstate', () => this.handleUrlChange());
// Monitor URL changes for SPA navigation
setInterval(() => {
if (window.location.href !== this.currentUrl) {
this.handleUrlChange();
}
}, 1000);
// Continue monitoring follower count
setInterval(() => this.updateFollowerCount(), 2000);
}
private handleUrlChange(): void {
const newUrl = window.location.href;
if (newUrl !== this.currentUrl) {
console.log('LinkedIn URL changed from', this.currentUrl, 'to', newUrl);
this.currentUrl = newUrl;
// Reset follower count for new profile
this.followerCount = "";
// Start monitoring follower count for new page
this.updateFollowerCount();
}
}
private updateFollowerCount() {
const followerElement = document.querySelector<HTMLElement>(ELEMENT_SELECTORS.profile.followerCount);
if (followerElement && followerElement.textContent && this.followerCount == "") {
const newCount = followerElement.textContent.trim();
if (newCount !== this.followerCount) {
this.followerCount = newCount;
console.log("Follower count updated:", this.followerCount);
}
}
}
tagElement(): void {
Object.keys(ELEMENT_SELECTORS).forEach((type) => {
const selectorObj = ELEMENT_SELECTORS[type as keyof typeof ELEMENT_SELECTORS];
Object.entries(selectorObj).forEach(([key, value]) => {
if (key === "showMore" && typeof value === 'string') {
const showMoreBtns = document.querySelectorAll<HTMLElement>(value);
showMoreBtns.forEach(btn => {
try {
// Check if button is visible and clickable before clicking
if (btn && btn.offsetParent !== null && !(btn as HTMLButtonElement).disabled) {
btn.click();
console.log("Clicked show more button in tagElement");
}
} catch (e) {
console.error("Error clicking show more button in tagElement:", e);
}
});
return;
}
if (Array.isArray(value)) {
value.forEach(sel => {
document.querySelectorAll<HTMLElement>(sel).forEach(element => this.addClassTagged(element));
});
} else {
document.querySelectorAll<HTMLElement>(value).forEach(element => this.addClassTagged(element));
}
});
});
}
async saveData(): Promise<model.FactCollection[]> {
const factCollections: model.FactCollection[] = [];
try {
if (this.isProfilePage()) {
await this.handleProfilePage(factCollections);
} else if (this.isPostPage()) {
await this.handlePostPage(factCollections);
}else if(this.isSearchPage()){
await this.handleProfilePage(factCollections);
}
} catch (error) {
console.error("Error in saveData:", error);
}
return factCollections;
}
private isProfilePage(): boolean {
return window.location.href.includes('/company/') || window.location.href.includes('/in/');
}
private isPostPage(): boolean {
return window.location.href.includes('/posts/') || window.location.href.includes('/feed/update/');
}
private isSearchPage(): boolean {
return window.location.href.includes('/search/results/');
}
private async handleProfilePage(factCollections: model.FactCollection[]): Promise<void> {
console.log("Extracting profile data...");
const profile = this.saveProfile();
if (profile.facts.length > 0) {
factCollections.push(profile);
this.markElementSaved(ELEMENT_SELECTORS.profile.container);
console.log("Profile data extracted successfully");
}
console.log("Extracting posts data...");
const posts = await this.savePost();
if (posts.facts.length > 0) {
factCollections.push(posts);
this.markElementSaved(ELEMENT_SELECTORS.post.container);
console.log(`${posts.facts.length} post facts extracted successfully`);
} else {
console.log("No posts found or extraction failed");
}
}
private async handlePostPage(factCollections: model.FactCollection[]): Promise<void> {
console.log("Extracting post data...");
const posts = await this.savePost();
if (posts.facts.length > 0) {
factCollections.push(posts);
this.markElementSaved(ELEMENT_SELECTORS.post.container);
console.log(`${posts.facts.length} post facts extracted successfully`);
}
console.log("Extracting comments data...");
const comments = this.saveComment();
if (comments.facts.length > 0) {
factCollections.push(comments);
this.markElementSaved(ELEMENT_SELECTORS.comment.container);
console.log(`${comments.facts.length} comment facts extracted successfully`);
}
}
private markElementSaved(selector: string): void {
const element = document.querySelector<HTMLElement>(selector) || document.body;
this.addClassSaved(element);
}
saveProfile(): model.FactCollection {
const profileContainer = document.querySelector<HTMLElement>(
ELEMENT_SELECTORS.profile.container
);
/* 1. Skip if already saved */
if (profileContainer && this.isSaved(profileContainer)) {
const profileData = model.NewFactCollection('profile');
return profileData;
}
/* 2. Build the fact collection */
const cleanedUrl = window.location.href.replaceAll('/recent-activity/all/', '');
const profile: model.FactCollection = {
url: window.location.href,
facts: [],
savedAt: new Date().toISOString(),
type: 'profile',
};
const elements = ELEMENT_SELECTORS.profile;
const profileUrl = model.NewFact('li_profile_source_url', 'url', cleanedUrl, cleanedUrl);
// Get display name directly
const displayNameElement = document.querySelector<HTMLElement>(elements.displayName);
const displayName = displayNameElement?.textContent?.trim() || '';
const displayNameFact = NewFact('li_profile_display_name', 'name', displayName, cleanedUrl);
// Get description directly
const descriptionElement = document.querySelector<HTMLElement>(elements.description);
const description = descriptionElement?.textContent?.trim() || '';
const descriptionFact = NewFact('li_profile_bio_text', 'text', description, cleanedUrl);
if (
displayNameFact?.value &&
descriptionFact?.value
) {
// Get profile image directly
const imgElement = document.querySelector<HTMLImageElement>(elements.image);
const profileImageSrc = imgElement?.getAttribute('src') || '';
const profileImageFact = NewFact('li_profile_profileimage_url', 'url', profileImageSrc, cleanedUrl);
// Get follower count directly
let followerCountFact;
if (!this.followerCount || this.followerCount.trim() === '') {
followerCountFact = NewFact('li_profile_follower_count', 'number', '0', cleanedUrl);
} else {
const cleanedText = this.followerCount.trim().replace(/[^\d.,]/g, '');
const numericValue = cleanedText.replace(/[.,]/g, '');
const followerCount = Number(numericValue) || 0;
followerCountFact = NewFact('li_profile_follower_count', 'number', followerCount.toString(), cleanedUrl);
}
const profileFacts: model.Fact[] = [
profileUrl,
displayNameFact,
descriptionFact,
profileImageFact,
followerCountFact,
];
profile.facts.push(...profileFacts);
/* 3. Mark container as saved only after success */
if (profileContainer) {
this.addClassSaved(profileContainer);
}
console.log('Profile data extracted successfully');
} else {
console.log('Profile extraction skipped missing required fields');
}
return profile;
}
async savePost(): Promise<model.FactCollection> {
const posts: model.FactCollection = window.location.href.includes('/search/') ? model.NewFactCollection('search') : model.NewFactCollection('post');
const selectors = ELEMENT_SELECTORS.post;
const postElements = document.querySelectorAll<HTMLElement>(`${selectors.container}:not(.saved)`);
// First, click all show more buttons and wait for content to load
await this.expandAllPostContent(postElements, selectors.showMore);
postElements.forEach((element) => {
const postFacts = this.extractPostFromElement(element, selectors);
posts.facts.push(...postFacts);
this.addClassSaved(element);
});
console.log(`Total post facts extracted: ${posts.facts.length}`);
return posts;
}
private async expandAllPostContent(postElements: NodeListOf<HTMLElement>, showMoreSelector: string): Promise<void> {
const clickPromises: Promise<void>[] = [];
postElements.forEach((element) => {
const btnShowMore = element.querySelectorAll<HTMLElement>(showMoreSelector);
btnShowMore.forEach(btn => {
// Check if button is visible and clickable
if (btn && btn.offsetParent !== null && !(btn as HTMLButtonElement).disabled) {
clickPromises.push(this.clickShowMoreButton(btn));
}
});
});
// Wait for all buttons to be clicked
await Promise.all(clickPromises);
// Additional wait to allow content to fully load after clicking
if (clickPromises.length > 0) {
await this.delay(500); // Wait 500ms for content to load
console.log(`Clicked ${clickPromises.length} show more buttons and waited for content to load`);
}
}
private async clickShowMoreButton(button: HTMLElement): Promise<void> {
return new Promise<void>((resolve) => {
try {
button.click();
// Small delay after each click to prevent overwhelming the page
setTimeout(resolve, 100);
} catch (e) {
console.error("Error clicking show more button:", e);
resolve();
}
});
}
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
saveComment(): model.FactCollection {
const comments: model.FactCollection = model.NewFactCollection('comment');
const selectors = ELEMENT_SELECTORS.comment;
const commentElements = document.querySelectorAll<HTMLElement>(`${selectors.container}:not(.saved)`);
commentElements.forEach(element => this.addClassTagged(element));
commentElements.forEach((element) => {
const commentFacts = this.extractCommentFromElement(element, selectors);
comments.facts.push(...commentFacts);
this.addClassSaved(element);
});
console.log(`Total comment facts extracted: ${comments.facts.length}`);
return comments;
}
addClassTagged(element: HTMLElement): void {
element.classList.add('tagged');
}
addClassSaved(element: HTMLElement): void {
element.classList.add('saved');
}
private extractPostFromElement(element: HTMLElement, selectors: typeof ELEMENT_SELECTORS.post): model.Fact[] {
const facts: model.Fact[] = [];
const urnEl = element.querySelector<HTMLElement>(selectors.url);
const urn = urnEl?.getAttribute('data-urn') || '';
const contextUrl = `https://www.linkedin.com/feed/update/${urn}`;
if (urn) {
facts.push(NewFact('li_post_source_url', 'url', contextUrl, contextUrl));
}
const displayNameEl = element.querySelector<HTMLElement>(selectors.displayName);
if (displayNameEl?.textContent?.trim()) {
facts.push(NewFact('li_post_display_name', 'name', displayNameEl.textContent.trim(), contextUrl));
}
const usernameEl = element.querySelector<HTMLElement>(selectors.username);
if (usernameEl?.textContent?.trim()) {
facts.push(NewFact('li_post_user_name', 'name', usernameEl.textContent.trim(), contextUrl));
}
const profileImageEl = element.querySelector<HTMLElement>(selectors.profileImage);
if (profileImageEl?.getAttribute('src')) {
const profileImageUrl = profileImageEl.getAttribute('src') || '';
facts.push(NewFact('li_post_profileimage_url', 'url', profileImageUrl, contextUrl));
}
const content = this.extractContent(element, [...selectors.content]);
if (content) {
facts.push(NewFact('li_post_content_md', 'md', content, contextUrl));
}
const likeCount = this.extractLikeCount(element, [...selectors.like]);
if (likeCount) {
facts.push(NewFact('li_post_like_count', 'number', likeCount, contextUrl));
}
const commentCount = this.extractCommentCount(element, selectors.comment);
if (commentCount) {
facts.push(NewFact('li_post_comment_count', 'number', commentCount, contextUrl));
}
const shareCount = this.extractShareCount(element, selectors.share);
if (shareCount) {
facts.push(NewFact('li_post_share_count', 'number', shareCount, contextUrl));
}
const date = this.extractDate(selectors.date)
// example value: "3hr"
if (date) {
facts.push(NewFact('li_post_duration_string', 'string', date, contextUrl));
}
return facts;
}
private extractCommentFromElement(element: HTMLElement, selectors: typeof ELEMENT_SELECTORS.comment): model.Fact[] {
const profileUrlEl = element.querySelector<HTMLElement>(selectors.urlProfile);
const profileUrl = profileUrlEl?.getAttribute('href')
const cleanedUrl = profileUrl?.replace('https://www.linkedin.com/in/', '').replace("/", "")
const contextUrl = `${window.location.href.replace(/\/$/, '')}#comment_by=${cleanedUrl}`;
const facts: model.Fact[] = [];
const displayNameEl = element.querySelector<HTMLElement>(selectors.displayName);
if (displayNameEl?.textContent?.trim()) {
facts.push(NewFact('li_comment_display_name', 'name', displayNameEl.textContent.trim(), contextUrl));
}
const profileImageEl = element.querySelector<HTMLElement>(selectors.profileImage);
if (profileImageEl?.getAttribute('src')) {
const profileImageUrl = profileImageEl.getAttribute('src') || '';
facts.push(NewFact('li_comment_profileimage_url', 'url', profileImageUrl, contextUrl));
}
const content = this.extractContent(element, [...selectors.content]);
if (content) {
facts.push(NewFact('li_comment_content_md', 'md', content, contextUrl));
}
const likeEl = element.querySelector<HTMLElement>(selectors.like);
if (likeEl?.textContent?.trim()) {
// Hapus semua karakter non-digit kecuali titik dan koma, lalu hapus pemisah ribuan
const cleanedText = likeEl.textContent.trim().replace(/[^\d.,]/g, '');
const numericValue = cleanedText.replace(/[.,]/g, ''); // Hapus titik dan koma
const likeCount = Number(numericValue) || 0;
facts.push(NewFact('li_comment_like_count', 'number', likeCount.toString(), contextUrl));
} else {
facts.push(NewFact('li_comment_like_count', 'number', "0", contextUrl));
}
const commentCountEl = element.querySelector<HTMLElement>(selectors.commentCount);
if (commentCountEl?.textContent?.trim()) {
const commentText = commentCountEl.textContent.trim().split(' ')[0];
// Hapus semua karakter non-digit kecuali titik dan koma, lalu hapus pemisah ribuan
const cleanedText = commentText.replace(/[^\d.,]/g, '');
const numericValue = cleanedText.replace(/[.,]/g, ''); // Hapus titik dan koma
const commentCount = Number(numericValue) || 0;
facts.push(NewFact('li_comment_comment_count', 'number', commentCount.toString(), contextUrl));
} else {
facts.push(NewFact('li_comment_comment_count', 'number', "0", contextUrl));
}
const dateEl = element.querySelector<HTMLElement>(selectors.date);
if (dateEl && dateEl.textContent) {
const dateText = dateEl.textContent.trim();
facts.push(NewFact('li_comment_duration_string', 'string', dateText, contextUrl));
}
return facts;
}
private extractDate(selector: string): string {
const dateEl = document.querySelector<HTMLElement>(selector);
if (dateEl && dateEl.textContent) {
const dateText = dateEl.textContent.trim();
return dateText;
}
return ""
}
private extractContent(element: HTMLElement, contentSelectors: string[]): string {
let textContent = '';
const mediaUrls: string[] = [];
for (const contentSelector of contentSelectors) {
const contentEls = element.querySelectorAll(contentSelector);
contentEls.forEach(el => {
if (!el.querySelector('img') && !el.querySelector('video') &&
el.tagName !== 'IMG' && el.tagName !== 'VIDEO') {
if (el.textContent && el.textContent.trim() !== '') {
textContent += el.outerHTML;
}
}
this.extractVideoUrls(el, mediaUrls);
this.extractImageUrls(el, mediaUrls);
});
}
const markdownText = textContent ? turndownService.turndown(textContent) : '';
const uniqueMediaUrls = this.cleanMediaUrls(mediaUrls);
let mediaContent = '';
if (uniqueMediaUrls.length > 0) {
mediaContent = uniqueMediaUrls.map((url: string) => `[photo](${url})`).join('\n');
}
return mediaContent + markdownText;
}
private extractLikeCount(element: HTMLElement, likeSelectors: string[]): string | null {
for (const likeSelector of likeSelectors) {
const likeEl = element.querySelector<HTMLElement>(likeSelector);
if (likeEl?.textContent?.trim()) {
// Hapus semua karakter non-digit kecuali titik dan koma, lalu hapus pemisah ribuan
const cleanedText = likeEl.textContent.trim().replace(/[^\d.,]/g, '');
const numericValue = cleanedText.replace(/[.,]/g, ''); // Hapus titik dan koma
const likeCount = Number(numericValue) || 0;
return likeCount.toString();
}
}
return null;
}
private extractCommentCount(element: HTMLElement, commentSelector: string): string | null {
const commentEl = element.querySelector<HTMLElement>(commentSelector);
if (commentEl?.getAttribute('aria-label')) {
const ariaLabel = commentEl.getAttribute('aria-label')?.split(' ')[0].trim() || '0';
// Hapus semua karakter non-digit kecuali titik dan koma, lalu hapus pemisah ribuan
const cleanedText = ariaLabel.replace(/[^\d.,]/g, '');
const numericValue = cleanedText.replace(/[.,]/g, ''); // Hapus titik dan koma
const commentCount = Number(numericValue) || 0;
return commentCount.toString();
}
return null;
}
private extractShareCount(element: HTMLElement, shareSelector: string): string | null {
const shareEl = element.querySelector<HTMLElement>(shareSelector);
if (shareEl?.textContent?.trim()) {
const shareText = shareEl.textContent.trim().split(' ')[0] || '0';
// Hapus semua karakter non-digit kecuali titik dan koma, lalu hapus pemisah ribuan
const cleanedText = shareText.replace(/[^\d.,]/g, '');
const numericValue = cleanedText.replace(/[.,]/g, ''); // Hapus titik dan koma
const shareCount = Number(numericValue) || 0;
return shareCount.toString();
}
return null;
}
private extractVideoUrls(element: Element, mediaUrls: string[]): void {
if (element.tagName === 'VIDEO') {
const posterUrl = element.getAttribute('poster');
if (posterUrl && !mediaUrls.includes(posterUrl)) {
mediaUrls.push(posterUrl);
}
return;
}
const videoElements = element.querySelectorAll('video');
videoElements.forEach(videoEl => {
const posterUrl = videoEl.getAttribute('poster');
if (posterUrl && !mediaUrls.includes(posterUrl)) {
mediaUrls.push(posterUrl);
}
});
}
private extractImageUrls(element: Element, mediaUrls: string[]): void {
if (element.tagName === 'IMG') {
const imgSrc = element.getAttribute('src');
if (imgSrc && !mediaUrls.includes(imgSrc)) {
mediaUrls.push(imgSrc);
}
return;
}
const imgElements = element.querySelectorAll('img');
imgElements.forEach(imgEl => {
const imgSrc = imgEl.getAttribute('src');
if (imgSrc && !mediaUrls.includes(imgSrc)) {
mediaUrls.push(imgSrc);
}
});
}
private cleanMediaUrl(url: string): string {
try {
return url.split('?')[0];
} catch (e) {
return url;
}
}
private cleanMediaUrls(urls: string[]): string[] {
const uniqueUrls: string[] = [];
for (const url of urls) {
if (!uniqueUrls.includes(url)) {
uniqueUrls.push(url);
}
}
const cleanedUrls: string[] = [];
const cleanedBaseUrls: string[] = [];
for (const url of uniqueUrls) {
const baseUrl = this.cleanMediaUrl(url);
if (!cleanedBaseUrls.includes(baseUrl)) {
cleanedBaseUrls.push(baseUrl);
cleanedUrls.push(url);
}
}
return cleanedUrls;
}
isTagged(element: HTMLElement): boolean {
return element.classList.contains('tagged');
}
isSaved(element: HTMLElement): boolean {
return element.classList.contains('saved');
}
}

705
src/modules/tiktok.ts Normal file
View File

@@ -0,0 +1,705 @@
import * as model from "../model";
import { NewFact } from "../model";
import { turndownService } from "../utils/turndown";
import { parseCount } from "../helper/parseCount";
const ELEMENT_SELECTORS = {
profile: {
ld: 'script[id="__UNIVERSAL_DATA_FOR_REHYDRATION__"]',
},
post: {
container: "div[data-e2e$='item-list'] div[data-e2e$='item']",
url: "div[data-e2e$='post-item'] a[href*='/video/'], div[data-e2e$='post-item'] a[href*='/photo/'], div[data-e2e$='search_top-item'] a[href*='/@']",
profileImage: '[data-e2e="browse-user-avatar"] img[src]',
view: '[data-e2e="video-views"]',
content: ["[data-e2e='browse-video'] > div", "[data-e2e='browse-video-desc']", "picture > img"],
username: '[data-e2e="browse-username"]',
displayName: '[data-e2e="browser-nickname"] span:first-child',
date: '[data-e2e="browser-nickname"] span:last-child',
like: '[data-e2e="browse-like-count"], [data-e2e="like-count"]',
share: '[data-e2e="share-count"]',
comment: '[data-e2e="browse-comment-count"], [data-e2e="comment-count"]',
},
postVideo: {
container: "div[class*='DivBrowserModeContainer']",
profileImage: '[data-e2e="browse-user-avatar"] img[src]',
content: "[data-e2e='browse-video-desc']",
videoUrl: "[data-e2e='browse-video'] > div > video[src",
view: `a[href*="${window.location.href}"] [data-e2e="video-views"]`,
username: 'div[class*="DivDescriptionContentWrapper"] a:nth-child(2) > span:first-child',
displayName: 'div[class*="DivDescriptionContentWrapper"] a:nth-child(2) > span:nth-child(3) > span:first-child',
date: 'div[class*="DivDescriptionContentWrapper"] a:nth-child(2) > span:nth-child(3) > span:last-child',
likeCount: '[data-e2e="browse-like-count"], [data-e2e="like-count"]',
commentCount: '[data-e2e="browse-comment-count"], [data-e2e="comment-count"]',
shareCount: '[data-e2e="share-count"]',
bookmarkCount: '[data-e2e="undefined-count"]',
},
postPhoto: {
container: "[class*='DivBrowserModeContainer'], [class*='DivPlayerContainer']",
content: "[data-e2e='browse-video-desc']",
photoUrl: "[class*='DivPhotoVideoContainer']",
profileImage: '[data-e2e="browse-user-avatar"] img[src]',
username: 'div[class*="DivDescriptionContentWrapper"] a:nth-child(2) > span:first-child, [data-e2e="browse-username"]',
displayName: 'div[class*="DivDescriptionContentWrapper"] a:nth-child(2) > span:nth-child(3) > span:first-child, [data-e2e="browser-nickname"] span:first-child',
date: 'div[class*="DivDescriptionContentWrapper"] a:nth-child(2) > span:nth-child(3) > span:last-child, [data-e2e="browser-nickname"] span:last-child',
like: '[data-e2e="browse-like-count"], [data-e2e="like-count"]',
view: `a[href*="${window.location.href}"] [data-e2e="video-views"]`,
share: '[data-e2e="share-count"]',
bookmarkCount: '[data-e2e="undefined-count"]',
comment: '[data-e2e="browse-comment-count"], [data-e2e="comment-count"]',
},
comment: {
container: 'div[class*="DivCommentItemContainer"], div[class*="DivCommentObjectWrapper"]',
url: 'div[class*="DivContentContainer"] > a',
content: '[data-e2e="comment-level-1"] span, [data-e2e="comment-level-1"] p',
username: '[data-e2e="comment-avatar-1"], [data-e2e="comment-username-1"] > div > a ',
displayName: '[data-e2e="comment-username-1"]',
date: '[data-e2e="comment-time-1"], [class*="DivCommentSubContentWrapper"] > span:first-child',
commentCount: '[class*="DivReplyActionContainer"] > p, [class*="DivViewRepliesContainer"] > span',
like: '[data-e2e="comment-like-count"], [aria-label*="likes"] span',
}
} as const;
export default class TiktokModule {
context: string = "";
tagElement(): void {
Object.keys(ELEMENT_SELECTORS).forEach((type) => {
const selectorObj = ELEMENT_SELECTORS[type as keyof typeof ELEMENT_SELECTORS];
Object.entries(selectorObj).forEach(([key, value]) => {
if (Array.isArray(value)) {
value.forEach(sel => {
document.querySelectorAll<HTMLElement>(sel).forEach(element => this.addClassTagged(element));
});
} else {
document.querySelectorAll<HTMLElement>(value).forEach(element => this.addClassTagged(element));
}
});
});
}
async saveData(): Promise<model.FactCollection[]> {
const factCollections: model.FactCollection[] = [];
try {
if (this.isSearchPage()) {
console.log("Detected search page");
await this.handleSearchPage(factCollections);
} else if (this.isProfilePage()) {
await this.handleProfilePage(factCollections);
} else if (this.isVideoPage()) {
await this.handleVideoPage(factCollections);
} else if (this.isPhotoPage()) {
await this.handlePhotoPage(factCollections);
}
} catch (error) {
console.error("Error in saveData:", error);
}
return factCollections;
}
private async handleSearchPage(factCollections: model.FactCollection[]): Promise<void> {
const posts = await this.savePost();
if (posts.facts.length > 0) {
factCollections.push(posts);
this.markElementSaved(ELEMENT_SELECTORS.post.container);
}
return;
}
private isProfilePage(): boolean {
return !window.location.href.includes('/video/') && !window.location.href.includes('/photo/');
}
private isSearchPage(): boolean {
return window.location.href.includes('/search');
}
private isVideoPage(): boolean {
return window.location.href.includes('/video/');
}
private isPhotoPage(): boolean {
return window.location.href.includes('/photo/');
}
private async handleProfilePage(factCollections: model.FactCollection[]): Promise<void> {
const profile = this.saveProfile();
if (profile.facts.length > 0) {
factCollections.push(profile);
}
const posts = await this.savePost();
if (posts.facts.length > 0) {
factCollections.push(posts);
this.markElementSaved(ELEMENT_SELECTORS.post.container);
}
}
private async handleVideoPage(factCollections: model.FactCollection[]): Promise<void> {
const posts = await this.savePost();
if (posts.facts.length > 0) {
factCollections.push(posts);
this.markElementSaved(ELEMENT_SELECTORS.post.container);
}
const comments = this.saveComment();
if (comments.facts.length > 0) {
factCollections.push(comments);
this.markElementSaved(ELEMENT_SELECTORS.comment.container);
}
}
private async handlePhotoPage(factCollections: model.FactCollection[]): Promise<void> {
const posts = await this.savePost();
if (posts.facts.length > 0) {
factCollections.push(posts);
this.markElementSaved(ELEMENT_SELECTORS.post.container);
}
const comments = this.saveComment();
if (comments.facts.length > 0) {
factCollections.push(comments);
this.markElementSaved(ELEMENT_SELECTORS.comment.container);
}
}
private markElementSaved(selector: string): void {
const element = document.querySelector<HTMLElement>(selector) || document.body;
this.addClassSaved(element);
}
saveProfile(): model.FactCollection {
const profile: model.FactCollection = model.NewFactCollection('profile');
profile.facts.push(NewFact('tt_profile_source_url', 'url', window.location.href, window.location.href));
const scriptTag = document.querySelector<HTMLScriptElement>(`${ELEMENT_SELECTORS.profile.ld}:not(.saved)`);
if (!scriptTag || !scriptTag.textContent) {
return profile;
}
try {
const data = JSON.parse(scriptTag.textContent);
const userInfo = data?.__DEFAULT_SCOPE__?.['webapp.user-detail']?.userInfo;
if (userInfo && userInfo.user) {
// Direct NewFact calls for profile data
const username = userInfo.user?.uniqueId || '';
if (username) {
profile.facts.push(NewFact('tt_profile_user_name', 'name', username, window.location.href));
}
const displayName = userInfo.user?.nickname || '';
if (displayName) {
profile.facts.push(NewFact('tt_profile_display_name', 'name', displayName, window.location.href));
}
const description = userInfo.user?.signature || '';
if (description) {
profile.facts.push(NewFact('tt_profile_bio_text', 'text', description, window.location.href));
}
const image = userInfo.user?.avatarLarger || userInfo.user?.avatarThumb || '';
if (image) {
profile.facts.push(NewFact('tt_profile_profileimage_url', 'url', image, window.location.href));
}
const followerCount = userInfo.stats?.followerCount?.toString() || '0';
profile.facts.push(NewFact('tt_profile_follower_count', 'number', followerCount, window.location.href));
const followingCount = userInfo.stats?.followingCount?.toString() || '0';
profile.facts.push(NewFact('tt_profile_following_count', 'number', followingCount, window.location.href));
const postCount = userInfo.stats?.videoCount?.toString() || '0';
profile.facts.push(NewFact('tt_profile_post_count', 'number', postCount, window.location.href));
const likeCount = userInfo.stats?.heartCount?.toString() || '0';
profile.facts.push(NewFact('tt_profile_like_count', 'number', likeCount, window.location.href));
this.addClassSaved(scriptTag);
}
} catch (error) {
console.error("Error parsing TikTok profile:", error);
}
return profile;
}
async savePost(): Promise<model.FactCollection> {
const posts: model.FactCollection = window.location.href.includes('/search/') ? model.NewFactCollection('search') : model.NewFactCollection('post');
if (this.isVideoPage()) {
await this.extractVideoPost(posts);
} else if (this.isPhotoPage()) {
await this.extractPhotoPost(posts);
} else {
await this.extractAllPosts(posts);
}
return posts;
}
private async extractVideoPost(posts: model.FactCollection): Promise<void> {
const scriptTag = document.querySelector<HTMLScriptElement>("script#__UNIVERSAL_DATA_FOR_REHYDRATION__:not(.saved)");
let dataExtracted = false;
if (scriptTag && scriptTag.textContent) {
try {
const data = JSON.parse(scriptTag.textContent);
const stats = data?.['__DEFAULT_SCOPE__']['webapp.video-detail']["itemInfo"]["itemStruct"] ||
data?.['__DEFAULT_SCOPE__']['webapp.video-detail']["itemInfo"]["itemStruct"]["stats"];
const url = window.location.href;
const urlInJson = data?.['__DEFAULT_SCOPE__']['seo.abtest']['canonical'];
if (stats && url === urlInJson) {
this.createPostFacts(stats, posts, url);
this.addClassSaved(document.body);
dataExtracted = true;
scriptTag.classList.add('saved');
} else {
console.warn("No stats found in video detail data or URL mismatch");
}
} catch (error) {
console.error("Error parsing TikTok video post:", error);
}
}
if (!dataExtracted) {
this.extractVideoPostFromDOM(posts);
}
}
private extractVideoPostFromDOM(posts: model.FactCollection): void {
const url = window.location.href;
const container = document.querySelector<HTMLElement>(`${ELEMENT_SELECTORS.postVideo.container}:not(.saved)`);
const selectors = ELEMENT_SELECTORS.postVideo;
if (!container) {
console.warn("❌ No container found for video post extraction or already saved");
return;
}
posts.facts.push(NewFact('tt_post_source_url', 'url', url, url));
const displayName = this.getDisplayName(container, selectors.displayName) ||
this.getDisplayName(document, selectors.displayName);
if (displayName) {
posts.facts.push(NewFact('tt_post_display_name', 'name', displayName, url));
}
const username = this.getUsername(container, selectors.username) ||
this.getUsername(document, selectors.username);
if (username) {
posts.facts.push(NewFact('tt_post_user_name', 'name', username, url));
posts.facts.push(NewFact('tt_post_profile_url', 'url', `https://www.tiktok.com/@${this.extractUsernameFromUrl(username)}`, url));
}
const profileImageElement = document.querySelector<HTMLImageElement>(selectors.profileImage);
if (profileImageElement?.src) {
posts.facts.push(NewFact('tt_post_profileimage_url', 'url', profileImageElement.src, url));
}
const content = this.getContent(container, selectors.content) ||
this.getContent(document, selectors.content);
if (content) {
posts.facts.push(NewFact('tt_post_content_md', 'md', content, url));
}
const viewElement = document.querySelector<HTMLElement>(`a[href="${window.location.href}"] [data-e2e="video-views"]`) ||
document.querySelector<HTMLElement>(selectors.view);
if (viewElement?.textContent) {
const viewCount = parseCount(viewElement.textContent.trim());
posts.facts.push(NewFact('tt_post_view_count', 'number', viewCount, url));
}
const likeCount = this.getInteractionCount(container, selectors.likeCount) ||
this.getInteractionCount(document, selectors.likeCount);
if (likeCount) {
posts.facts.push(NewFact('tt_post_like_count', 'number', likeCount, url));
}
const shareCount = this.getInteractionCount(container, selectors.shareCount) ||
this.getInteractionCount(document, selectors.shareCount);
if (shareCount) {
posts.facts.push(NewFact('tt_post_share_count', 'number', shareCount, url));
}
const commentCount = this.getInteractionCount(container, selectors.commentCount) ||
this.getInteractionCount(document, selectors.commentCount);
if (commentCount) {
posts.facts.push(NewFact('tt_post_comment_count', 'number', commentCount, url));
}
const bookmarkCount = this.getInteractionCount(container, selectors.bookmarkCount) ||
this.getInteractionCount(document, selectors.bookmarkCount);
if (bookmarkCount) {
posts.facts.push(NewFact('tt_post_bookmark_count', 'number', bookmarkCount, url));
}
const date = container.querySelector<HTMLElement>(selectors.date)?.textContent?.trim();
if (date) {
posts.facts.push(NewFact('tt_post_duration_string', 'string', date, url));
}
this.addClassSaved(container);
}
private extractUsernameFromUrl(url: string): string {
const match = url.match(/@([^\/]+)/);
return match ? match[1] : 'unknown';
}
private async extractPhotoPost(posts: model.FactCollection): Promise<void> {
const container = document.querySelector<HTMLElement>(`${ELEMENT_SELECTORS.postPhoto.container}:not(.saved)`);
if (!container) {
console.warn("❌ No container found for photo post extraction or already saved");
return;
}
const url = window.location.href;
const selectors = ELEMENT_SELECTORS.postPhoto;
posts.facts.push(NewFact('tt_post_source_url', 'url', url, url));
const displayName = this.getDisplayName(document, selectors.displayName);
if (displayName) posts.facts.push(NewFact('tt_post_display_name', 'name', displayName, url));
const username = this.getUsername(document, selectors.username);
if (username) posts.facts.push(NewFact('tt_post_user_name', 'name', username, url));
const profileImageElement = document.querySelector<HTMLImageElement>(selectors.profileImage);
if (profileImageElement?.src) {
posts.facts.push(NewFact('tt_post_profileimage_url', 'url', profileImageElement.src, url));
}
const content = (container ? this.getContent(container, selectors.content) : null) ||
this.getContent(document, selectors.content);
if (content) posts.facts.push(NewFact('tt_post_content_md', 'md', content, url));
const viewCount = document.querySelector<HTMLElement>(selectors.view)?.textContent?.trim();
const viewCountText = viewCount ? parseCount(viewCount) : "0";
if (viewCount) posts.facts.push(NewFact('tt_post_view_count', 'number', viewCountText, url));
const likeCount = this.getInteractionCount(document, selectors.like);
if (likeCount) posts.facts.push(NewFact('tt_post_like_count', 'number', likeCount, url));
const shareCount = this.getInteractionCount(document, selectors.share);
if (shareCount) posts.facts.push(NewFact('tt_post_share_count', 'number', shareCount, url));
const commentCount = this.getInteractionCount(document, selectors.comment);
if (commentCount) posts.facts.push(NewFact('tt_post_comment_count', 'number', commentCount, url));
const bookmarkCount = this.getInteractionCount(document, selectors.bookmarkCount);
if (bookmarkCount) posts.facts.push(NewFact('tt_post_bookmark_count', 'number', bookmarkCount, url));
const date = container.querySelector<HTMLElement>(selectors.date)?.textContent?.trim() || "";
if (date) posts.facts.push(NewFact('tt_post_duration_string', 'string', date, url));
this.addClassSaved(container);
}
private extractAllPosts(posts: model.FactCollection): void {
const postElements = document.querySelectorAll<HTMLElement>('[id^="column-item-video-container"]:not(.saved)');
for (const element of postElements) {
let postUrl = '';
const fullUrlEl = element.querySelector<HTMLElement>(ELEMENT_SELECTORS.post.url);
if (fullUrlEl) {
const href = fullUrlEl.getAttribute('href') || '';
postUrl = href.startsWith('http') ? href : `https://www.tiktok.com${href}`;
posts.facts.push(NewFact('tt_post_source_url', 'url', href, href));
}
const profileUrl = element.querySelector<HTMLElement>('[data-e2e="search-card-user-link"]')?.getAttribute('href') || '';
if (profileUrl) {
const profileLink = profileUrl.startsWith('http') ? profileUrl : `https://www.tiktok.com${profileUrl}`;
posts.facts.push(NewFact('tt_post_profile_url', 'url', profileLink, postUrl));
}else if(!window.location.href.includes('/search/')){
const profileLink = window.location.href;
posts.facts.push(NewFact('tt_post_profile_url', 'url', profileLink, postUrl));
}
const userName = element.querySelector<HTMLElement>('[data-e2e="search-card-user-link"]')?.getAttribute('href')?.replace('@', '').replace("/", '').trim();
if (userName) {
posts.facts.push(NewFact('tt_post_user_name', 'name', userName, postUrl));
}else if(!window.location.href.includes('/search/')){
const userName = window.location.href.split('@').pop() || '';
posts.facts.push(NewFact('tt_post_user_name', 'name', userName, postUrl));
}
this.addClassSaved(element);
}
}
saveComment(): model.FactCollection {
const comments: model.FactCollection = model.NewFactCollection('comment');
const selectors = ELEMENT_SELECTORS.comment;
const commentElements = document.querySelectorAll<HTMLElement>(`${selectors.container}:not(.saved)`);
commentElements.forEach((element) => {
if (element.classList.contains('saved')) {
console.warn("❌ Comment element already saved, skipping:", element);
return;
}
const elements = {
displayName: element.querySelector<HTMLElement>(selectors.displayName),
username: element.querySelector<HTMLElement>(selectors.username),
content: element.querySelector<HTMLElement>(selectors.content),
likeCount: element.querySelector<HTMLElement>(selectors.like),
commentCount: element.querySelector<HTMLElement>(selectors.commentCount),
date: element.querySelector<HTMLElement>(selectors.date),
};
if (elements.displayName && elements.content) {
this.extractCommentFacts(elements, comments, element);
element.classList.add('saved');
}
});
return comments;
}
private extractCommentFacts(
elements: {
displayName: HTMLElement | null;
username: HTMLElement | null;
content: HTMLElement | null;
likeCount: HTMLElement | null;
commentCount: HTMLElement | null;
date: HTMLElement | null;
},
comments: model.FactCollection,
element: HTMLElement
): void {
const username = elements.username?.getAttribute('href')?.replace('@', '').replace("/", '').trim();
const contextUrl = window.location.href + `#comment_by=${username}`;
const displayName = elements.displayName?.innerText.trim();
if (displayName) {
comments.facts.push(NewFact('tt_comment_display_name', 'name', displayName, contextUrl));
}
if (username) {
comments.facts.push(NewFact('tt_comment_user_name', 'name', username.replace('@', '').replace("/", ''), contextUrl));
}
const content = elements.content?.innerText.trim();
if (content) {
comments.facts.push(NewFact('tt_comment_content_md', 'md', content, contextUrl));
}
const likeCount = elements.likeCount ? parseCount(elements.likeCount.innerText.trim()) : "0";
comments.facts.push(NewFact('tt_comment_like_count', 'number', likeCount, contextUrl));
if (elements.commentCount) {
const commentCountText = elements.commentCount.innerText.trim();
const digitsOnly = commentCountText.split(" ")[1];
const commentCount = parseCount(digitsOnly);
if (commentCount) {
comments.facts.push(NewFact('tt_comment_comment_count', 'number', commentCount, contextUrl));
}
}
const dateText = elements.date?.textContent?.trim();
if (dateText) {
comments.facts.push(NewFact('tt_comment_duration_string', 'string', dateText, contextUrl));
}
}
private createPostFacts(itemStruct: any, posts: model.FactCollection, url: string): void {
posts.facts.push(NewFact('tt_post_source_url', 'url', url, url));
let displayName = '';
let username = '';
const author = itemStruct.statsV2?.author || itemStruct.author || {};
if (author.nickname) {
displayName = author.nickname;
} else {
displayName = this.extractUsernameFromUrl(url);
}
if (author.uniqueId) {
username = author.uniqueId;
} else {
username = this.extractUsernameFromUrl(url);
}
if (displayName) {
posts.facts.push(NewFact('tt_post_display_name', 'name', displayName, url));
}
if (username) {
posts.facts.push(NewFact('tt_post_user_name', 'name', username, url));
}
if (itemStruct.author.avatarLarger) {
posts.facts.push(NewFact('tt_post_profileimage_url', 'url', itemStruct.author.avatarLarger, url));
}
if (itemStruct.desc) {
let contentFormat = '';
const videoUrl = itemStruct.video?.bitrateInfo?.[0]?.PlayAddr?.UrlList?.[1] ||
itemStruct.video?.playAddr || '';
const content = itemStruct.desc.trim();
if (videoUrl && content) {
contentFormat = `[videoUrl](${videoUrl})\n\n${content}`;
} else if (videoUrl) {
contentFormat = `[videoUrl](${videoUrl})`;
} else if (content) {
contentFormat = turndownService.turndown(content);
}
if (contentFormat) {
posts.facts.push(NewFact('tt_post_content_md', 'md', contentFormat, url));
}
}
const stats = itemStruct.statsV2 || itemStruct.stats || {};
if (stats.diggCount !== undefined) {
const likeCount = parseCount(stats.diggCount.toString());
posts.facts.push(NewFact('tt_post_like_count', 'number', likeCount, url));
}
if (stats.commentCount !== undefined) {
const commentCount = parseCount(stats.commentCount.toString());
posts.facts.push(NewFact('tt_post_comment_count', 'number', commentCount, url));
}
if (stats.shareCount !== undefined) {
const shareCount = parseCount(stats.shareCount.toString());
posts.facts.push(NewFact('tt_post_share_count', 'number', shareCount, url));
}
if (stats.playCount !== undefined) {
const viewCount = parseCount(stats.playCount.toString());
posts.facts.push(NewFact('tt_post_view_count', 'number', viewCount, url));
}
if (stats.collectCount !== undefined) {
const bookmarkCount = parseCount(stats.collectCount.toString());
posts.facts.push(NewFact('tt_post_bookmark_count', 'number', bookmarkCount, url));
}
if (itemStruct.createTime) {
const date = new Date(itemStruct.createTime * 1000).toISOString();
posts.facts.push(NewFact('tt_post_createdat_datetime', 'datetime', date, url));
}
}
private getDisplayName(element: HTMLElement | Document, selector: string): string | null {
const displayNameEl = element.querySelector<HTMLElement>(selector);
return displayNameEl ? displayNameEl.innerText.trim() : null;
}
private getUsername(element: HTMLElement | Document, selector: string): string | null {
const usernameEl = element.querySelector<HTMLElement>(selector);
return usernameEl ? usernameEl.innerText.replace('@', '').replace("/", '').trim() : null;
}
private getContent(element: HTMLElement | Document, contentSelector: string, mediaSelector?: string): string | null {
let content = '';
const contentEl = element.querySelector<HTMLElement>(contentSelector);
if (contentEl && contentEl.textContent) {
content = contentEl.textContent.trim();
}
const mediaItems: string[] = [];
if (this.isVideoPage()) {
const videoSrc = this.getVideoSrc(element, ELEMENT_SELECTORS.postVideo.videoUrl);
if (videoSrc) {
if (videoSrc.startsWith('blob:')) {
mediaItems.push(`[video-blob](${videoSrc})`);
} else {
mediaItems.push(`[videoUrl](${videoSrc})`);
}
}
}
if (this.isPhotoPage()) {
const photoUrls = this.getPhotoUrls(element, ELEMENT_SELECTORS.postPhoto.photoUrl);
photoUrls.forEach((photoUrl, index) => {
mediaItems.push(`[photo](${photoUrl})`);
});
}
let fullContent = '';
if (mediaItems.length > 0 && content) {
const mediaLine = mediaItems.join(' ');
fullContent = mediaLine + '\n\n' + content;
} else if (mediaItems.length > 0) {
fullContent = mediaItems.join(' ');
} else if (content) {
fullContent = turndownService.turndown(content);
}
return fullContent || null;
}
private getPhotoUrls(element: HTMLElement | Document, photoSelector: string): string[] {
const photoUrls: string[] = [];
const uniqueUrls = new Set<string>();
const photoContainer = element.querySelector<HTMLElement>(photoSelector);
if (photoContainer) {
const images = photoContainer.querySelectorAll('img');
images.forEach((img) => {
const src = img.src || img.getAttribute('data-src') || img.getAttribute('srcset')?.split(' ')[0] || '';
if (src && !uniqueUrls.has(src)) {
uniqueUrls.add(src);
photoUrls.push(src);
}
});
}
return photoUrls;
}
private getVideoSrc(element: HTMLElement | Document, videoSelector: string): string | null {
const videoEl = element.querySelector<HTMLVideoElement>(videoSelector);
if (videoEl && videoEl.src) {
return videoEl.src;
}
const anyVideoEl = element.querySelector<HTMLVideoElement>('video[src]');
if (anyVideoEl && anyVideoEl.src) {
return anyVideoEl.src;
}
return null;
}
private getInteractionCount(element: HTMLElement | Document, selector: string): string | null {
const interactionEl = element.querySelector<HTMLElement>(selector);
if (!interactionEl) return null;
const textContent = interactionEl.textContent;
if (!textContent) return null;
const count = parseCount(textContent.replace(/\D/g, ''));
return count || '0';
}
addClassTagged(element: HTMLElement): void {
element.classList.add('tagged');
}
addClassSaved(element: HTMLElement): void {
element.classList.add('saved');
}
}

721
src/modules/x.ts Normal file
View File

@@ -0,0 +1,721 @@
import * as model from '../model';
import { NewFact } from '../model';
import { turndownService } from '../utils/turndown';
const ELEMENT_SELECTORS = {
profile: {
ld: 'script[type="application/ld+json"]'
},
post: {
container: '[data-testid="tweet"]',
showMore: 'button[data-testid="tweet-text-show-more-link"]',
url: '[data-testid="tweet"] a[href*="status"]:first-of-type',
profileImage: 'img[src*="profile_images"]',
content: "[data-testid='tweetText']",
imageUrl: "[data-testid='tweetPhoto'] img",
videoUrl: "[data-testid='tweetPhoto'] video",
like: "button[data-testid='like']",
share: "button[data-testid='retweet']",
comment: "button[data-testid='reply']",
bookmark: "button[data-testid='bookmark']",
views: "a[aria-label*='views']",
date: "article time",
displayName: "[data-testid='User-Name'] > div > div > a > div > div span",
username: "[data-testid='User-Name'] > div:nth-child(2) > div > div > a > div span",
},
comment: {
container: '[data-testid="tweet"]',
url: '[data-testid="tweet"] a[href*="status"]:first-of-type',
content: "[data-testid='tweetText']",
profileImage: 'img[src*="profile_images"]',
imageUrl: "[data-testid='tweetPhoto'] img",
videoUrl: "[data-testid='tweetPhoto'] video",
like: "button[data-testid='like']",
share: "button[data-testid='retweet']",
commentCount: "button[data-testid='reply']",
bookmark: "button[data-testid='bookmark']",
views: "a[aria-label*='views']",
date: "article time",
displayName: "[data-testid='User-Name'] > div > div > a > div > div span",
username: "[data-testid='User-Name'] > div:nth-child(2) > div > div > a > div span",
}
} as const;
export default class XModule {
context: string = "";
tagElement(): void {
console.log(window.location.host);
Object.keys(ELEMENT_SELECTORS).forEach((type) => {
const selectorObj = ELEMENT_SELECTORS[type as keyof typeof ELEMENT_SELECTORS];
Object.entries(selectorObj).forEach(([key, value]) => {
if (Array.isArray(value)) {
value.forEach(sel => {
document.querySelectorAll<HTMLElement>(sel).forEach(element => this.addClassTagged(element));
});
} else if (key === "showMore") {
const showMoreButton = document.querySelectorAll<HTMLElement>(value);
showMoreButton.forEach(button => {
this.addClassTagged(button);
button.click();
});
} else {
document.querySelectorAll<HTMLElement>(value).forEach(element => this.addClassTagged(element));
}
});
});
}
async saveData(): Promise<model.FactCollection[]> {
const factCollections: model.FactCollection[] = [];
try {
if (this.isStatusPage()) {
await this.handleStatusPage(factCollections);
} else {
await this.handleProfilePage(factCollections);
}
} catch (error) {
console.error("Error in saveData:", error);
}
return factCollections;
}
private isStatusPage(): boolean {
return window.location.href.includes('/status/');
}
private async handleProfilePage(factCollections: model.FactCollection[]): Promise<void> {
const profile = this.saveProfile();
if (profile.facts.length > 0) {
factCollections.push(profile);
this.markElementSaved(ELEMENT_SELECTORS.profile.ld);
}
// Expand all show more buttons and wait for content to load
await this.expandAllPostContent();
// Then save the posts with expanded content
const posts = await this.savePost();
if (posts.facts.length > 0) {
factCollections.push(posts);
this.markElementSaved(ELEMENT_SELECTORS.post.container);
}
}
private async expandAllPostContent(): Promise<void> {
const showMoreButtons = document.querySelectorAll<HTMLElement>(ELEMENT_SELECTORS.post.showMore);
if (showMoreButtons.length === 0) {
console.log("No show more buttons found");
return;
}
console.log(`Found ${showMoreButtons.length} show more buttons to expand`);
// Click all buttons first
showMoreButtons.forEach(button => {
this.addClassTagged(button);
button.click();
});
// Wait for all content to expand
await new Promise(resolve => setTimeout(resolve, 1500));
console.log("All post content expansion completed");
}
private async handleStatusPage(factCollections: model.FactCollection[]): Promise<void> {
// Expand all show more buttons first
await this.expandAllPostContent();
const posts = await this.savePost();
if (posts.facts.length > 0) {
factCollections.push(posts);
this.markElementSaved(ELEMENT_SELECTORS.post.container);
}
const comments = await this.saveComment();
if (comments.facts.length > 0) {
factCollections.push(comments);
}
}
private markElementSaved(selector: string): void {
const element = document.querySelector<HTMLElement>(selector) || document.body;
this.addClassSaved(element);
}
saveProfile(): model.FactCollection {
const collection: model.FactCollection = model.NewFactCollection('profile');
collection.url = window.location.href;
const ldScript = document.querySelector<HTMLScriptElement>(`${ELEMENT_SELECTORS.profile.ld}:not(.saved)`);
if (!ldScript || !ldScript.textContent) {
return collection;
}
try {
const jsonData = JSON.parse(ldScript.textContent);
if (jsonData["@type"] === "ProfilePage" && jsonData.mainEntity && !ldScript.classList.contains('saved')) {
this.extractProfileFacts(jsonData.mainEntity, collection);
this.addClassSaved(ldScript);
}
} catch (error) {
console.error("Error parsing LD+JSON:", error);
}
return collection;
}
private extractProfileFacts(mainEntity: any, collection: model.FactCollection): void {
const contextUrl = collection.url;
collection.facts.push(NewFact('x_profile_source_url', 'url', contextUrl, contextUrl));
const username = mainEntity.additionalName || this.extractUsernameFromUrl(mainEntity.url) || '';
if (username) {
collection.facts.push(NewFact('x_profile_user_name', 'name', username, contextUrl));
}
const displayName = mainEntity.givenName || mainEntity.name || '';
if (displayName) {
collection.facts.push(NewFact('x_profile_display_name', 'name', displayName, contextUrl));
}
const description = mainEntity.description || '';
if (description) {
collection.facts.push(NewFact('x_profile_bio_md', 'md', description, contextUrl));
}
this.extractProfileImage(mainEntity.image, collection, contextUrl);
if (mainEntity.interactionStatistic && Array.isArray(mainEntity.interactionStatistic)) {
this.extractInteractionStatistics(mainEntity.interactionStatistic, collection, contextUrl);
}
}
private extractProfileImage(image: any, collection: model.FactCollection, contextUrl: string): void {
if (!image) return;
let imageUrl = '';
if (typeof image === "string") {
imageUrl = image;
} else if (typeof image === "object" && image.contentUrl) {
imageUrl = image.contentUrl;
}
if (imageUrl) {
collection.facts.push(NewFact('x_profile_profileimage_url', 'url', imageUrl, contextUrl));
}
}
private extractInteractionStatistics(statistics: any[], collection: model.FactCollection, contextUrl: string): void {
statistics.forEach((stat: any) => {
const count = stat.userInteractionCount?.toString() || '0';
if (stat.interactionType === "https://schema.org/FollowAction" && stat.name === "Follows") {
collection.facts.push(NewFact('x_profile_follower_count', 'number', count, contextUrl));
} else if (stat.interactionType === "https://schema.org/SubscribeAction" && stat.name === "Friends") {
collection.facts.push(NewFact('x_profile_following_count', 'number', count, contextUrl));
} else if (stat.interactionType === "https://schema.org/WriteAction" && stat.name === "Tweets") {
collection.facts.push(NewFact('x_profile_post_count', 'number', count, contextUrl));
}
});
}
async savePost(): Promise<model.FactCollection> {
const posts: model.FactCollection = model.NewFactCollection('post');
const selectors = ELEMENT_SELECTORS.post;
if (this.isStatusPage()) {
const conversationTimeline = document.querySelector<HTMLElement>('[aria-label="Timeline: Conversation"]:not(.saved)');
console.log('Conversation Timeline:', conversationTimeline);
if (conversationTimeline) {
const postElements = conversationTimeline.querySelector<HTMLElement>(`${selectors.container}:not(.saved)`);
await this.extractFromPostElement(postElements, posts, selectors);
this.addClassSaved(conversationTimeline);
} else {
console.warn('No conversation timeline found on status page.');
}
} else {
const postElements = document.querySelectorAll<HTMLElement>(`${selectors.container}:not(.saved)`);
postElements.forEach(element => this.addClassTagged(element));
await this.extractFromPostElements(postElements, posts, selectors);
}
return posts;
}
private async extractFromPostElements(
postElements: NodeListOf<HTMLElement> | HTMLElement[],
posts: model.FactCollection,
selectors: typeof ELEMENT_SELECTORS.post
): Promise<void> {
await Promise.all(Array.from(postElements).map(async (element) => {
try {
const urlEl = element.querySelector<HTMLElement>(selectors.url);
if (!urlEl) return;
const href = urlEl.getAttribute('href');
if (!href) return;
const fullUrl = href.startsWith('http') ? href : `https://x.com${href}`;
const facts = this.createFactsFromExtractedData(element, selectors, fullUrl);
posts.facts.push(...facts);
this.addClassSaved(element);
} catch (error) {
console.error('Error processing post element:', error);
}
}));
}
private async extractFromPostElement(
postElement: HTMLElement | null,
posts: model.FactCollection,
selectors: typeof ELEMENT_SELECTORS.post
): Promise<void> {
if (!postElement) return;
try {
const urlEl = postElement.querySelector<HTMLElement>(selectors.url);
if (!urlEl) return;
const href = urlEl.getAttribute('href');
if (!href) return;
const fullUrl = href.startsWith('http') ? href : `https://x.com${href}`;
const facts = this.createFactsFromExtractedData(postElement, selectors, fullUrl);
posts.facts.push(...facts);
this.addClassSaved(postElement);
} catch (error) {
console.error('Error processing post element:', error);
}
}
private createFactsFromExtractedData(
element: HTMLElement,
selectors: typeof ELEMENT_SELECTORS.post,
fullUrl: string
): model.Fact[] {
const facts: model.Fact[] = [];
const contextUrl = fullUrl;
facts.push(NewFact('x_post_source_url', 'url', fullUrl, contextUrl));
const postData = this.extractPostData(element, selectors);
if (postData.profileUrl) facts.push(NewFact('x_post_profile_url', 'url', postData.profileUrl, contextUrl));
if (postData.displayName) facts.push(NewFact('x_post_display_name', 'name', postData.displayName, contextUrl));
if (postData.username) facts.push(NewFact('x_post_user_name', 'name', postData.username, contextUrl));
if (postData.profileImage) facts.push(NewFact('x_post_profileimage_url', 'url', postData.profileImage, contextUrl));
if (postData.content) facts.push(NewFact('x_post_content_md', 'md', postData.content, contextUrl));
if (postData.likeCount !== null) facts.push(NewFact('x_post_like_count', 'number', postData.likeCount, contextUrl));
if (postData.shareCount !== null) facts.push(NewFact('x_post_share_count', 'number', postData.shareCount, contextUrl));
if (postData.commentCount !== null) facts.push(NewFact('x_post_comment_count', 'number', postData.commentCount, contextUrl));
if (postData.bookmarkCount !== null) facts.push(NewFact('x_post_bookmark_count', 'number', postData.bookmarkCount, contextUrl));
if (postData.viewCount !== null) facts.push(NewFact('x_post_view_count', 'number', postData.viewCount, contextUrl));
if (postData.date) facts.push(NewFact('x_post_createdat_datetime', 'datetime', postData.date, contextUrl));
return facts;
}
private extractPostData(element: HTMLElement, selectors: typeof ELEMENT_SELECTORS.post): {
displayName: string | null;
profileUrl: string | null;
username: string | null;
profileImage: string | null;
content: string | null;
likeCount: string | null;
shareCount: string | null;
commentCount: string | null;
bookmarkCount: string | null;
viewCount: string | null;
date: string | null;
} {
return {
displayName: this.getDisplayName(element, selectors.displayName),
username: this.getUsername(element, selectors.username),
profileUrl: `https://x.com${element.querySelector<HTMLElement>('div[data-testid="User-Name"] a')?.getAttribute('href')}` || null,
profileImage: this.getProfileImage(element, selectors.profileImage),
content: this.getContent(element, {
content: selectors.content,
imageUrl: selectors.imageUrl,
videoUrl: selectors.videoUrl,
}),
likeCount: this.getInteractionCount(element, selectors.like),
shareCount: this.getInteractionCount(element, selectors.share),
commentCount: this.getInteractionCount(element, selectors.comment),
bookmarkCount: this.getBookmarkCount(element, selectors.bookmark),
viewCount: this.getViewCount(element, selectors.views),
date: this.getDate(element, selectors.date),
};
}
async saveComment(): Promise<model.FactCollection> {
const comments: model.FactCollection = model.NewFactCollection('comment');
if (!this.isStatusPage()) {
return comments;
}
const selectors = ELEMENT_SELECTORS.comment;
let commentElements: HTMLElement[] = [];
const conversationTimeline = document.querySelector('[aria-label="Timeline: Conversation"]');
if (conversationTimeline) {
const allArticlesInConversation = conversationTimeline.querySelectorAll<HTMLElement>('[data-testid="tweet"]');
const commentsInConversation = Array.from(allArticlesInConversation).slice(1).filter(element => {
return !element.classList.contains('saved');
});
commentElements.push(...commentsInConversation);
console.log(`Found ${allArticlesInConversation.length} total articles in conversation, ${commentsInConversation.length} comments in conversation`);
}
const allTweetElements = document.querySelectorAll<HTMLElement>(`${selectors.container}:not(.saved)`);
const outsideCommentElements = Array.from(allTweetElements).filter(element => {
const conversationContainer = element.closest('[aria-label="Timeline: Conversation"]');
return conversationContainer === null;
});
commentElements.push(...outsideCommentElements);
commentElements.forEach(element => this.addClassTagged(element));
await Promise.all(commentElements.map(async (element) => {
try {
const urlEl = element.querySelector<HTMLElement>(selectors.url);
if (!urlEl) return;
const href = urlEl.getAttribute('href');
if (!href) return;
const fullUrl = href.startsWith('http') ? href : `https://x.com${href}`;
const pageContext = window.location.href; // context halaman status/thread
const facts = this.createCommentFactsFromExtractedData(element, selectors, fullUrl, pageContext);
if (facts?.length > 0) {
comments.facts.push(...facts);
}
this.addClassSaved(element);
} catch (error) {
console.error('Error processing comment element:', error);
}
}));
return comments;
}
private createCommentFactsFromExtractedData(
element: HTMLElement,
selectors: typeof ELEMENT_SELECTORS.comment,
fullUrl: string,
pageContext: string
): model.Fact[] {
const facts: model.Fact[] = [];
const commentData = this.extractCommentData(element, selectors);
if (!commentData.date) return facts;
// comment URL dengan context = halaman saat ini
facts.push(NewFact('x_comment_source_url', 'url', fullUrl, fullUrl));
// fakta lain memakai context = URL komentar itu sendiri
const contextUrl = fullUrl;
if (commentData.profileUrl) facts.push(NewFact('x_comment_profile_url', 'url', commentData.profileUrl, contextUrl));
if (commentData.date) facts.push(NewFact('x_comment_createdat_datetime', 'datetime', commentData.date, contextUrl));
if (commentData.displayName) facts.push(NewFact('x_comment_display_name', 'name', commentData.displayName, contextUrl));
if (commentData.username) facts.push(NewFact('x_comment_user_name', 'name', commentData.username, contextUrl));
if (commentData.profileImage) facts.push(NewFact('x_comment_profileimage_url', 'url', commentData.profileImage, contextUrl));
if (commentData.content) facts.push(NewFact('x_comment_content_md', 'md', commentData.content, contextUrl));
if (commentData.likeCount !== null) facts.push(NewFact('x_comment_like_count', 'number', commentData.likeCount, contextUrl));
if (commentData.shareCount !== null) facts.push(NewFact('x_comment_share_count', 'number', commentData.shareCount, contextUrl));
if (commentData.commentCount !== null) facts.push(NewFact('x_comment_comment_count', 'number', commentData.commentCount, contextUrl));
if (commentData.bookmarkCount !== null) facts.push(NewFact('x_comment_bookmark_count', 'number', commentData.bookmarkCount, contextUrl));
if (commentData.viewCount !== null) facts.push(NewFact('x_comment_view_count', 'number', commentData.viewCount, contextUrl));
return facts;
}
private extractCommentData(element: HTMLElement, selectors: typeof ELEMENT_SELECTORS.comment): {
displayName: string | null;
username: string | null;
profileImage: string | null;
profileUrl: string | null;
content: string | null;
likeCount: string | null;
shareCount: string | null;
commentCount: string | null;
bookmarkCount: string | null;
viewCount: string | null;
date: string | null;
} {
return {
displayName: this.getDisplayName(element, selectors.displayName),
username: this.getUsername(element, selectors.username),
profileUrl: `https://x.com${element.querySelector<HTMLElement>('div[data-testid="User-Name"] a')?.getAttribute('href')}` || null,
profileImage: this.getProfileImage(element, selectors.profileImage),
content: this.getContent(element, {
content: selectors.content,
imageUrl: selectors.imageUrl,
videoUrl: selectors.videoUrl,
}),
likeCount: this.getInteractionCount(element, selectors.like),
shareCount: this.getInteractionCount(element, selectors.share),
commentCount: this.getInteractionCount(element, selectors.commentCount),
viewCount: this.getViewCount(element, selectors.views),
bookmarkCount: this.getBookmarkCount(element, selectors.bookmark),
date: this.getDate(element, selectors.date),
};
}
private getDisplayName(element: HTMLElement, selector: string): string | null {
const displayNameEl = element.querySelector<HTMLElement>(selector);
return displayNameEl ? displayNameEl.innerText.trim() : null;
}
private getUsername(element: HTMLElement, selector: string): string | null {
const usernameEl = element.querySelector<HTMLElement>(selector);
return usernameEl ? usernameEl.innerText.replace('@', '').trim() : null;
}
private getBookmarkCount(element: HTMLElement, selector: string): string | null {
const bookmarkEl = element.querySelector<HTMLElement>("[aria-label*='bookmark']");
if (!bookmarkEl) return null;
const ariaLabel = bookmarkEl.getAttribute('aria-label');
if (!ariaLabel) return null;
const parts = ariaLabel.split(', ');
const bookmarkPart = parts.find(part => part.includes('bookmark'));
if (bookmarkPart) {
const bookmarkValue = bookmarkPart.trim().split(' ')[0];
const count = parseInt(bookmarkValue || '0');
return isNaN(count) ? '0' : count.toString();
}
return '0';
}
private buildFinalContent(
media: { type: 'photo' | 'video'; url: string }[],
text: string | null
): string | null {
const lines: string[] = [];
media.forEach(({ type, url }) => {
lines.push(`[${type}Url](${url})`);
});
const trimmedText = text?.trim();
if (trimmedText) {
if (lines.length) lines.push('');
lines.push(trimmedText);
}
return lines.join('\n').trim() || null;
}
private getContent(
element: HTMLElement,
selectors: Pick<typeof ELEMENT_SELECTORS.post, 'content' | 'imageUrl' | 'videoUrl'>
): string | null {
const lines: string[] = [];
const photoEls = element.querySelectorAll<HTMLImageElement>(selectors.imageUrl);
photoEls.forEach(img => lines.push(`[photoUrl](${img.src})`));
const videoEls = element.querySelectorAll<HTMLVideoElement>(selectors.videoUrl);
videoEls.forEach(v => {
if (v.poster) lines.push(`[videoUrl](${v.poster})`);
const src = v.querySelector('source')?.src || v.src;
if (src && !src.startsWith('blob:')) lines.push(`[videoUrl](${src})`);
});
const textEl = element.querySelector<HTMLElement>(selectors.content);
const text = textEl ? turndownService.turndown(textEl.innerHTML).trim() : null;
if (text) {
if (lines.length) lines.push('');
lines.push(text);
}
return lines.join('\n').trim() || null;
}
private hasVideoIndicator(element: HTMLElement): boolean {
const videoIndicators = [
'[data-testid="videoPlayer"]',
'[data-testid="videoComponent"]',
'[data-testid="videoControls"]'
];
for (const selector of videoIndicators) {
if (element.querySelector(selector)) {
console.log(`Found video indicator: ${selector}`);
return true;
}
}
const tweetPhoto = element.querySelector('[data-testid="tweetPhoto"]');
if (tweetPhoto) {
const playElements = [
'svg[aria-label*="Play" i]',
'button[aria-label*="Play" i]',
'button[aria-label*="Pause" i]',
'button[aria-label*="Watch again" i]',
'[aria-label*="Play video" i]'
];
for (const selector of playElements) {
const playElement = tweetPhoto.querySelector(selector);
if (playElement) {
console.log(`Found video play element: ${selector}`, playElement);
return true;
}
}
const videoElement = tweetPhoto.querySelector('video');
if (videoElement) {
console.log('Found video element:', videoElement);
return true;
}
}
console.log('No video indicators found - treating as image content');
return false;
}
getProfileImage(element: HTMLElement, selector: string): string {
const imgEl = element.querySelector<HTMLImageElement>(selector);
if (!imgEl) return '';
const imgSrc = imgEl.src.trim();
return imgSrc || '';
}
private isVideoThumbnail(img: HTMLImageElement): boolean {
const parent = img.closest('[data-testid="tweetPhoto"]');
if (!parent) return false;
const hasVideoIndicators = parent.querySelector('[aria-label*="Play"]') ||
parent.querySelector('svg') ||
parent.querySelector('[role="button"]');
const videoThumbnailPatterns = [
'amplify_video_thumb',
'video_thumb',
'ext_tw_video_thumb',
'thumbnail',
'poster',
'preview'
];
const imgSrc = img.src.toLowerCase();
const hasVideoPattern = videoThumbnailPatterns.some(pattern => imgSrc.includes(pattern));
return !!(hasVideoIndicators || hasVideoPattern);
}
private hasStrongVideoIndicators(img: HTMLImageElement): boolean {
const parent = img.closest('[data-testid="tweetPhoto"]');
if (!parent) return false;
const hasExplicitPlayButton = parent.querySelector('svg[aria-label*="Play video" i]') ||
parent.querySelector('button[aria-label*="Play video" i]') ||
parent.querySelector('[data-testid="videoPlayer"]') ||
parent.querySelector('[data-testid="videoComponent"]');
const hasVideoElement = parent.querySelector('video');
const result = !!(hasExplicitPlayButton || hasVideoElement);
if (result) {
console.log('Image has STRONG video indicators:', {
hasExplicitPlayButton: !!hasExplicitPlayButton,
hasVideoElement: !!hasVideoElement,
imgSrc: img.src
});
}
return result;
}
private getInteractionCount(element: HTMLElement, selector: string): string | null {
const interactionEl = element.querySelector<HTMLElement>(selector);
if (!interactionEl) return null;
const ariaLabel = interactionEl.getAttribute('aria-label');
if (!ariaLabel) return null;
const countStr = ariaLabel.split(' ')[0];
const count = parseInt(countStr || '0');
return isNaN(count) ? '0' : count.toString();
}
private getViewCount(element: HTMLElement, selector: string): string | null {
let viewEl = element.querySelector<HTMLElement>(selector);
if (viewEl && viewEl.getAttribute('aria-label')) {
const ariaLabel = viewEl.getAttribute('aria-label');
const viewValue = ariaLabel?.split(' ')[0] || '0';
return viewValue;
}
viewEl = element.querySelector<HTMLElement>("[aria-label*='views']");
if (viewEl) {
const ariaLabel = viewEl.getAttribute('aria-label');
if (ariaLabel) {
const parts = ariaLabel.split(', ');
const viewsPart = parts.find(part => part.includes('views'));
if (viewsPart) {
const viewValue = viewsPart.trim().split(' ')[0];
return viewValue;
}
}
}
viewEl = element.querySelector<HTMLElement>("span[data-testid='app-text-transition-container']");
if (viewEl && viewEl.textContent) {
return viewEl.textContent.trim();
}
return null;
}
private getDate(element: HTMLElement, selector: string): string | null {
const dateEl = element.querySelector<HTMLElement>(selector);
return dateEl ? dateEl.getAttribute('datetime') || null : null;
}
extractUsernameFromUrl(url: string): string | null {
const match = url?.match(/x\.com\/([^\/]+)$/);
return match ? match[1] : null;
}
addClassTagged(element: HTMLElement): void {
element.classList.add('x-tagged');
}
addClassSaved(element: HTMLElement): void {
element.classList.add('saved');
}
isTagged(element: HTMLElement): boolean {
return element.classList.contains('x-tagged');
}
isSaved(element: HTMLElement): boolean {
return element.classList.contains('saved');
}
}

1000
src/modules/youtube.ts Normal file

File diff suppressed because it is too large Load Diff

128
src/popup/popup.css Normal file
View File

@@ -0,0 +1,128 @@
:root {
--primary-color: #333;
--background-color: #f8f9fa;
--text-color: #333;
--border-color: #e0e0e0;
--button-hover-color: #333;
--panel-width: 20rem;
}
body {
width: var(--panel-width);
max-width: var(--panel-width);
min-width: var(--panel-width);
min-height: 100vh;
background: var(--background-color);
color: var(--text-color);
font-family: Arial, sans-serif;
margin: 0;
padding: 1rem;
display: flex;
flex-direction: column;
align-items: stretch;
justify-content: flex-start;
gap: 0.625rem;
box-sizing: border-box;
height: 100vh;
overflow-x: hidden;
}
button {
background-color: var(--primary-color);
color: white;
border: none;
padding: 0.625rem 1.25rem;
border-radius: 0.313rem;
cursor: pointer;
transition: background-color 0.3s;
margin: 0.625rem 0;
width: 100%;
font-size: 1rem;
text-align: center;
box-sizing: border-box;
}
button:hover {
background-color: var(--button-hover-color);
}
.btn-menu {
width: 30px;
height: 30px;
justify-content: end;
display: flex;
}
#output {
width: 100%;
max-width: 100%;
box-sizing: border-box;
overflow-x: hidden;
word-wrap: break-word;
}
#output pre {
max-width: 100%;
overflow-x: auto;
overflow-y: auto;
max-height: 18.75rem;
font-size: 0.625rem;
line-height: 1.2;
white-space: pre-wrap;
word-break: break-all;
box-sizing: border-box;
}
.menu-container {
position: absolute;
height: auto;
width: 8.125rem;
right: 0.625rem;
display: none;
top: 3.75rem;
background-color: white;
border: 0.063rem solid #ccc;
padding: 0.625rem;
justify-content: left;
border-radius: 0.625rem;
}
.menu-container>ul {
justify-content: center;
display: flex;
flex-direction: column;
list-style: none;
padding: 0;
margin: 0;
gap: 0.313rem;
}
.menu-container>ul>li {
gap: 1px;
cursor: pointer;
list-style: none;
font-family: Arial, sans-serif;
font-size: 0.813rem;
display: flex;
align-items: center;
}
.menu-container>ul>li>a:hover {
background-color: #333333;
color: #fff;
}
.menu-container>ul>li>a {
text-decoration: none;
color: #333;
width: 100%;
display: block;
padding: 5px 5px;
border-radius: 5px;
font-weight: 400;
}
.active {
display: block;
}

51
src/popup/popup.html Normal file
View File

@@ -0,0 +1,51 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Popup</title>
<link rel="stylesheet" href="popup.css" />
</head>
<body>
<div id="header" style="display: flex; align-items: center; justify-content: space-between;">
<button id="toggle-sidebar-btn" style="width: 10px; align-items: center; display: flex; justify-content: center;border-radius: 50%; color: #333; background-color: #f8f9fa;">x</button>
<h2>New Browser Input</h1>
<a class="btn-menu">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round"
d="M17.982 18.725A7.488 7.488 0 0 0 12 15.75a7.488 7.488 0 0 0-5.982 2.975m11.963 0a9 9 0 1 0-11.963 0m11.963 0A8.966 8.966 0 0 1 12 21a8.966 8.966 0 0 1-5.982-2.275M15 9.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
</svg>
</a>
</div>
<!-- New toggle button -->
<div class="menu-container">
<ul>
<li>
<a href="">Profile</a>
</li>
<li>
<a href="">Settings</a>
</li>
<li>
<a href="">Logout</a>
</li>
</ul>
</div>
<button id="tag-btn" data-action="tagElement">Tag</button>
<button id="save-btn" data-action="saveData">Save Data</button>
<form action="" id="form-api-endpoint" style="display: flex; justify-content: center; align-items: center; height: 50px; margin-top: 10px;">
<input type="text" name="api-endpoint" id="field-api-endpoint" placeholder="Input Endpoint Api" id=""
style="width: 100%; padding: 10px; border: 1px solid #ccc; border-radius: 4px; margin-right: 10px;" />
<button type="submit" style=" font-size: 10px; width: 30%;">Save endpoint</button>
</form>
<hr>
<h3>
Output
</h3>
<div id="output"></div>
<script type="module" src="popup.js"></script>
</body>
</html>

190
src/popup/popup.ts Normal file
View File

@@ -0,0 +1,190 @@
type PopupActionHandler = () => void;
const popupActionHandlers: Record<string, PopupActionHandler> = {
tagElement: handleTagElement,
saveData: handleSaveData
}
// Main function to execute a popup action
interface ExecuteActionResponse {
success: boolean;
data?: unknown;
key?: string;
}
function executeAction(action: keyof typeof popupActionHandlers): void {
chrome.tabs.query({ active: true, currentWindow: true }, (tabs: chrome.tabs.Tab[]) => {
chrome.tabs.sendMessage(
tabs[0].id as number,
{ action },
(response: ExecuteActionResponse) => {
if (chrome.runtime.lastError) {
console.error('Gagal mengirim pesan:', chrome.runtime.lastError.message);
return;
}
if (response && response.success) {
console.log('Aksi berhasil:', response);
// Tampilkan data otomatis setelah save
if (action === 'saveData' && response.data) {
displaySavedData(response.data, response.key);
}
} else {
console.error('Aksi gagal:', response);
}
}
);
});
}
interface DisplaySavedDataParams {
data: unknown;
key?: string;
}
function displaySavedData(data: DisplaySavedDataParams['data'], key: DisplaySavedDataParams['key']): void {
const outputElement: HTMLElement | null = document.getElementById('output');
if (outputElement) {
let htmlContent = `
<div style="margin-top: 10px; padding: 10px; border: 1px solid #4CAF50; border-radius: 4px; background: #f9fff9; width: 100%; box-sizing: border-box;">
<h3 style="color: #4CAF50; margin: 0 0 10px 0; font-size: 14px; word-wrap: break-word;">✅ Data Berhasil Disimpan</h3>
<p style="margin: 5px 0; font-size: 11px; color: #666; word-wrap: break-word; overflow-wrap: break-word;"><strong>Key:</strong> ${key || 'N/A'}</p>
`;
// Check if data is an array of fact collections
if (Array.isArray(data)) {
data.forEach((collection: any, index: number) => {
htmlContent += `
<div style="margin: 15px 0; border: 1px solid #ddd; border-radius: 4px; overflow: hidden;">
<div style="background: #f0f0f0; padding: 8px; font-weight: bold; font-size: 12px;">
${collection.type || 'Collection'} ${index + 1} - ${collection.url || 'N/A'}
</div>
<div style="padding: 5px;">
<p style="margin: 5px 0; font-size: 10px; color: #666;">
<strong>Saved:</strong> ${collection.savedAt ? new Date(collection.savedAt).toLocaleString() : 'N/A'}
</p>
<div style="overflow-x: auto; max-width: 100%; border: 1px solid #ddd; border-radius: 4px;">
<table style="width: max-content; min-width: 100%; border-collapse: collapse; font-size: 10px; margin: 0;">
<thead>
<tr style="background: #f8f8f8;">
<th style="border: 1px solid #ddd; padding: 6px; text-align: left; font-weight: bold; min-width: 100px; white-space: nowrap;">Name</th>
<th style="border: 1px solid #ddd; padding: 6px; text-align: left; font-weight: bold; min-width: 80px; white-space: nowrap;">Type</th>
<th style="border: 1px solid #ddd; padding: 6px; text-align: left; font-weight: bold; min-width: 300px; white-space: nowrap;">Value</th>
<th style="border: 1px solid #ddd; padding: 6px; text-align: left; font-weight: bold; min-width: 200px; white-space: nowrap;">Context</th>
</tr>
</thead>
<tbody>
`;
// Add facts to table
if (collection.facts && Array.isArray(collection.facts)) {
collection.facts.forEach((fact: any) => {
const value = fact.value ? String(fact.value) : 'N/A';
const context = fact.context || 'N/A';
htmlContent += `
<tr>
<td style="border: 1px solid #ddd; padding: 6px; word-break: break-word; white-space: pre-wrap;">${fact.name || 'N/A'}</td>
<td style="border: 1px solid #ddd; padding: 6px; white-space: nowrap;">${fact.type || 'N/A'}</td>
<td style="border: 1px solid #ddd; padding: 6px; word-break: break-word; white-space: pre-wrap; max-width: 400px;">${value}</td>
<td style="border: 1px solid #ddd; padding: 6px; word-break: break-word; white-space: pre-wrap; max-width: 300px;">${context}</td>
</tr>
`;
});
}
htmlContent += `
</tbody>
</table>
</div>
</div>
</div>
`;
});
} else {
// Fallback for non-array data
htmlContent += `
<pre style="background: #f5f5f5; padding: 8px; border-radius: 4px; font-size: 10px; margin: 10px 0 0 0; white-space: pre-wrap; word-break: break-all; overflow-wrap: break-word; max-width: 100%; overflow-x: hidden; box-sizing: border-box; line-height: 1.4;">${JSON.stringify(data, null, 2)}</pre>
`;
}
htmlContent += `</div>`;
outputElement.innerHTML = htmlContent;
}
}
function handleTagElement(): void {
// Aksi untuk menandai elemen
// alert('Element tagged!');
executeAction('tagElement');
}
function handleSaveData(): void {
// Aksi untuk menyimpan data
executeAction('saveData');
}
document.addEventListener('DOMContentLoaded', () => {
const popupAction = document.querySelectorAll('[data-action]');
popupAction.forEach(button => {
button.addEventListener('click', () => {
const action = button.getAttribute('data-action');
if (action && action in popupActionHandlers) {
popupActionHandlers[action]();
} else {
console.error(`Unknown popup action: ${action}`);
}
})
});
const btnMenu = document.querySelector('.btn-menu');
if (btnMenu) {
btnMenu.addEventListener('click', () => {
const menu = document.querySelector('.menu-container');
if (menu) {
menu.classList.toggle('active');
}
});
}
const toggleBtn = document.getElementById('toggle-sidebar-btn');
if (toggleBtn) {
toggleBtn.addEventListener('click', () => {
// Ask the current tab to toggle the sidebar
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
chrome.tabs.sendMessage(tabs[0].id!, { action: 'toggleSidebar' });
});
});
}
const formApiEndpoint = document.getElementById('form-api-endpoint') as HTMLFormElement;
if (formApiEndpoint) {
formApiEndpoint.addEventListener('submit', (event) => {
event.preventDefault();
const input = formApiEndpoint.querySelector('input[name="api-endpoint"]') as HTMLInputElement;
const apiURL = input.value.trim();
if (apiURL.startsWith('http://') || apiURL.startsWith('https://')) {
// Save the API endpoint to storage
chrome.storage.local.set({ apiURL }, () => {
// console.log('API endpoint saved:', apiURL);
alert('API endpoint saved successfully!');
});
} else {
alert('API endpoint cannot be empty / must start with http:// or https://');
}
});
}
chrome.storage.local.get('apiURL', (result) => {
const apiField = document.getElementById('field-api-endpoint') as HTMLInputElement;
if (apiField) {
apiField.value = result.apiURL || '';
}
console.log('Current API endpoint:', result.apiURL);
});
});

65
src/public/manifest.json Normal file
View File

@@ -0,0 +1,65 @@
{
"manifest_version": 3,
"name": "New Browser Input",
"version": "1.0",
"description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed euismod, nisl nec ultricies lacinia, nunc nisl aliquet nunc, vitae aliquam nisl nunc vitae nisl. Sed euismod, nisl nec ultricies lacinia, nunc nisl aliquet nunc, vitae aliquam nisl nunc vitae nisl.",
"permissions": [
"storage",
"activeTab",
"scripting",
"webRequest",
"webRequestBlocking"
],
"host_permissions": [
"https://x.com/*",
"https://www.tiktok.com/*",
"https://www.linkedin.com/*",
"https://www.youtube.com/*",
"https://www.facebook.com/*",
"https://www.instagram.com/*"
],
"web_accessible_resources": [
{
"resources": ["popup/popup.html", "popup/popup.js", "popup/popup.css"],
"matches": [
"https://x.com/*",
"https://www.tiktok.com/*",
"https://www.linkedin.com/*",
"https://www.youtube.com/*",
"https://www.facebook.com/*",
"https://www.instagram.com/*"
]
}
],
"side_panel": {
"default_path": "popup/popup.html"
},
"content_scripts": [
{
"matches": [
"https://x.com/*",
"https://www.tiktok.com/*",
"https://www.linkedin.com/*",
"https://www.youtube.com/*",
"https://www.facebook.com/*",
"https://www.instagram.com/*"
],
"js": ["contentScript.js"],
"css": ["mark.css"]
}
],
"background": {
"service_worker": "background.js"
},
"action": {
"default_title": "New Browser Input",
"default_popup": "popup/popup.html"
}
}

219
src/utils/downloadVideo.ts Normal file
View File

@@ -0,0 +1,219 @@
// Versi TypeScript dengan dukungan File System dan tipe data
import fs from 'fs';
import https from 'https';
import http from 'http';
import { URL } from 'url';
import path from 'path';
// Tipe untuk response hasil download menggunakan https
interface HttpsDownloadResponse {
ok: boolean;
status: number;
headers: http.IncomingHttpHeaders;
arrayBuffer: () => Promise<ArrayBuffer>;
}
// Fungsi untuk download video
export async function downloadVideo(videoUrl: string, filename = 'video.mp4'): Promise<Buffer | null> {
try {
console.log('🔄 Downloading video from:', videoUrl);
let response: any;
if (typeof fetch !== 'undefined') {
response = await fetch(videoUrl);
} else {
response = await downloadWithHttps(videoUrl);
}
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const contentType = response.headers.get ?
response.headers.get('content-type') :
response.headers['content-type'];
console.log('📁 Content Type:', contentType);
const contentLength = response.headers.get ?
response.headers.get('content-length') :
response.headers['content-length'];
if (contentLength) {
console.log('📊 File Size:', (parseInt(contentLength) / 1024 / 1024).toFixed(2) + ' MB');
}
const videoBuffer = typeof fetch !== 'undefined' ?
Buffer.from(await response.arrayBuffer()) :
response as Buffer;
console.log('✅ Video downloaded successfully');
await saveVideoToFile(videoBuffer, filename);
return videoBuffer;
} catch (error: any) {
console.error('❌ Error downloading video:', error.message);
return null;
}
}
export function downloadWithHttps(videoUrl: string): Promise<HttpsDownloadResponse> {
return new Promise((resolve, reject) => {
const urlObj = new URL(videoUrl);
const protocol = urlObj.protocol === 'https:' ? https : http;
const request = protocol.get(videoUrl, (response) => {
if (response.statusCode !== 200) {
reject(new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`));
return;
}
const chunks: Buffer[] = [];
response.on('data', (chunk) => chunks.push(chunk));
response.on('end', () => {
const buffer = Buffer.concat(chunks);
resolve({
ok: true,
status: response.statusCode || 200,
headers: response.headers,
arrayBuffer: () => Promise.resolve(buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength))
});
});
});
request.on('error', reject);
request.setTimeout(30000, () => {
request.destroy();
reject(new Error('Request timeout'));
});
});
}
export async function saveVideoToFile(buffer: Buffer, filename: string): Promise<string> {
try {
const downloadsDir = path.join(process.cwd(), 'downloads');
if (!fs.existsSync(downloadsDir)) {
fs.mkdirSync(downloadsDir, { recursive: true });
}
const filePath = path.join(downloadsDir, filename);
await fs.promises.writeFile(filePath, buffer);
console.log('💾 File saved to:', filePath);
console.log('📁 File size:', (buffer.length / 1024 / 1024).toFixed(2) + ' MB');
return filePath;
} catch (error: any) {
console.error('❌ Error saving file:', error);
throw error;
}
}
export async function getVideoInfo(videoUrl: string): Promise<any> {
const urlObj = new URL(videoUrl);
const protocol = urlObj.protocol === 'https:' ? https : http;
return new Promise((resolve, reject) => {
const request = protocol.request(videoUrl, { method: 'HEAD' }, (response) => {
resolve({
url: videoUrl,
status: response.statusCode,
contentType: response.headers['content-type'],
contentLength: response.headers['content-length'],
lastModified: response.headers['last-modified'],
size: response.headers['content-length'] ?
(parseInt(response.headers['content-length']) / 1024 / 1024).toFixed(2) + ' MB' : 'Unknown'
});
});
request.on('error', reject);
request.setTimeout(10000, () => {
request.destroy();
reject(new Error('Request timeout'));
});
request.end();
});
}
export async function downloadVideoWithProgress(videoUrl: string, filename = 'video.mp4'): Promise<{ buffer: Buffer; filePath: string; size: number; } | null> {
try {
console.log('🔄 Starting download with progress tracking...');
const url = new URL(videoUrl);
const protocol = url.protocol === 'https:' ? https : http;
return new Promise((resolve, reject) => {
const request = protocol.get(videoUrl, (response) => {
if (response.statusCode !== 200) {
reject(new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`));
return;
}
const total = parseInt(response.headers['content-length'] || '0', 10);
let loaded = 0;
const chunks: Buffer[] = [];
console.log('📊 Total size:', (total / 1024 / 1024).toFixed(2) + ' MB');
response.on('data', (chunk) => {
chunks.push(chunk);
loaded += chunk.length;
if (total) {
const progress = Math.round((loaded / total) * 100);
const loadedMB = (loaded / 1024 / 1024).toFixed(2);
const totalMB = (total / 1024 / 1024).toFixed(2);
process.stdout.write(`\r📥 Progress: ${progress}% (${loadedMB}/${totalMB} MB)`);
}
});
response.on('end', async () => {
console.log('\n✅ Download completed!');
const buffer = Buffer.concat(chunks);
try {
const filePath = await saveVideoToFile(buffer, filename);
resolve({ buffer, filePath, size: buffer.length });
} catch (error) {
reject(error);
}
});
});
request.on('error', reject);
request.setTimeout(60000, () => {
request.destroy();
reject(new Error('Request timeout'));
});
});
} catch (error: any) {
console.error('Download failed:', error);
return null;
}
}
export async function testDownload(): Promise<void> {
const videoUrl = 'https://your.video.url.mp4';
console.log('=== Testing Video Download in Node.js ===');
try {
console.log('\n1. Getting video info...');
const info = await getVideoInfo(videoUrl);
console.log('Video Info:', info);
console.log('\n2. Downloading video with progress...');
const result = await downloadVideoWithProgress(videoUrl, 'instagram_video.mp4');
if (result) {
console.log('✅ Download completed!');
console.log('📁 File saved to:', result.filePath);
console.log('📊 File size:', (result.size / 1024 / 1024).toFixed(2) + ' MB');
}
} catch (error: any) {
console.error('❌ Test failed:', error.message);
}
}
if (import.meta.url === `file://${process.argv[1]}`) {
testDownload();
}

4
src/utils/turndown-plugin-gfm.d.ts vendored Normal file
View File

@@ -0,0 +1,4 @@
declare module 'turndown-plugin-gfm' {
import { Plugin } from 'turndown';
export const gfm: Plugin;
}

47
src/utils/turndown.ts Normal file
View File

@@ -0,0 +1,47 @@
import TurndownService from 'turndown';
import { gfm } from 'turndown-plugin-gfm';
export const turndownService = new TurndownService();
turndownService.use(gfm);
// bug table
const tableRule = turndownService.rules.array[2];
if (!tableRule.filter.toString().includes('TABLE'))
throw new Error('Incorrect rule selected. Expected to find table rule');
tableRule.filter = ['table'];
// Filter element button youtube
turndownService.addRule('filterButtonContainer', {
filter: function (node) {
return node.id === 'button-container';
},
replacement: function () {
return ''; // Hilangkan element ini sepenuhnya
}
});
// Filter element YouTube
turndownService.addRule('youtubeElements', {
filter: function (node) {
// Filter semua element yang dimulai dengan 'yt-' atau 'ytd-'
if (node.nodeName && (node.nodeName.toLowerCase().startsWith('yt-') ||
node.nodeName.toLowerCase().startsWith('ytd-'))) {
return true;
}
return false;
},
replacement: function (content) {
// Ambil teks dari dalam element YouTube
return content;
}
});
// Filter element yang tersembunyi
turndownService.addRule('hiddenElements', {
filter: function (node) {
return node.hasAttribute && node.hasAttribute('hidden');
},
replacement: function () {
return '';
}
});

60
tmp/timeConverter.js Normal file
View File

@@ -0,0 +1,60 @@
const timeConverter = (hoursAgo) => {
const date = new Date();
date.setTime(date.getTime() - (parseInt(hoursAgo) * 60 * 60 * 1000));
return {
indonesian: date.toLocaleString('id-ID', {
timeZone: 'Asia/Jakarta'
}),
iso: date.toISOString()
};
};
const parseRelativeTime = (relativeTimeString) => {
const match = relativeTimeString.match(/(\d+)(hr|h|hours?|min|m|minutes?|d|days?)\s*ago/i);
if (!match) {
throw new Error('Invalid relative time format');
}
const value = parseInt(match[1]);
const unit = match[2].toLowerCase();
const date = new Date();
switch (unit) {
case 'hr':
case 'h':
case 'hour':
case 'hours':
date.setTime(date.getTime() - (value * 60 * 60 * 1000));
break;
case 'min':
case 'm':
case 'minute':
case 'minutes':
date.setTime(date.getTime() - (value * 60 * 1000));
break;
case 'd':
case 'day':
case 'days':
date.setTime(date.getTime() - (value * 24 * 60 * 60 * 1000));
break;
default:
throw new Error('Unsupported time unit');
}
return {
indonesian: date.toLocaleString('id-ID', {
timeZone: 'Asia/Jakarta'
}),
iso: date.toISOString(),
timestamp: date.getTime()
};
};
console.log('4 hours ago:', parseRelativeTime('4hr ago'));
console.log('30 minutes ago:', parseRelativeTime('30min ago'));
console.log('2 days ago:', parseRelativeTime('2d ago'));
console.log('Original timeConverter:', timeConverter('4'));

16
tsconfig.json Normal file
View File

@@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Node",
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"ignoreDeprecations": "6.0"
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}