Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 46 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,47 +4,55 @@ A mobile-first, browser-only tracker for public X timelines, styled and structur

Live dashboard: <https://daggerok.github.io/twitter/>

## How the free collector works

X's free embedded timeline is unreliable and frequently returns HTTP 429. X's official data API is pay-per-use. This project therefore provides an optional logged-in-browser collector:

1. Install [Tampermonkey](https://www.tampermonkey.net/).
2. **Allow Chrome to execute userscripts:**
- Right-click Tampermonkey → **Manage extension**.
- Turn on **Allow User Scripts**. Chrome 138+ requires this even when Tampermonkey and the script both appear enabled.
- If the toggle is unavailable, enable **Developer mode** at `chrome://extensions`.
- Set Tampermonkey site access to **On all sites**, or at least allow `x.com`.
- See [Tampermonkey FAQ Q209](https://www.tampermonkey.net/faq.php?locale=en#Q209).
3. Install or update [`collector.user.js`](https://daggerok.github.io/twitter/collector.user.js). Confirm version **1.1.0**.
4. Log into X normally and open [`@I_Am_The_ICT` Posts & replies](https://x.com/I_Am_The_ICT/with_replies).
5. Hard-reload the X tab after installation: **Ctrl+Shift+R** on Windows/Linux or **Cmd+Shift+R** on macOS.
6. Confirm that the floating **𝕏 ICT Collector** panel appears at the bottom-right:
- **Start Manual** records rendered tweet cards while you scroll.
- **Start + Auto** performs slow, capped assisted scrolling.
- **Pause** stops immediately.
7. Choose **Copy New JSON** or **Download JSON**.
8. Return to the dashboard and use **📋 Paste** or the **📄 JSON import** dialog.

No X password, session cookie, authorization token, or private browser storage is copied to the dashboard. The collector observes only tweet cards rendered in the current X page.
## Free logged-in Chrome collector

X's free embedded timeline is unreliable and frequently returns HTTP 429. X's official data API is pay-per-use. This project therefore provides an unpacked Chromium extension that collects tweet cards rendered in your already logged-in X tab.

The Chrome extension replaces the earlier Tampermonkey userscript.

## Install the extension

1. Disable or remove the old **daggerok X Timeline Collector** userscript from Tampermonkey.
2. Download [`twitter-collector-extension.zip`](https://daggerok.github.io/twitter/twitter-collector-extension.zip).
3. Unzip it. The extracted `extension` folder must directly contain `manifest.json`.
4. Open `chrome://extensions`.
5. Enable **Developer mode** in the top-right.
6. Click **Load unpacked**.
7. Select the extracted `extension` folder—not the ZIP and not the repository root.
8. Pin **daggerok X Timeline Collector** to the browser toolbar.

The source is also available in [`extension/`](./extension/).

## Daily usage

1. Log into X normally and open [`@I_Am_The_ICT` Posts & replies](https://x.com/I_Am_The_ICT/with_replies).
2. Click the **daggerok X Timeline Collector** extension icon while the X tab is active.
3. The popup shows whether the collector content script is loaded. It automatically injects/reinjects the script when needed.
4. Choose:
- **Show panel** — display/reopen the floating controls.
- **Start Manual** — collect rendered tweet cards while you scroll.
- **Start Auto** — perform slow, capped assisted scrolling.
- **Pause** — stop immediately.
5. Choose **Copy New JSON** in the popup or floating panel, or download JSON from the panel.
6. Return to <https://daggerok.github.io/twitter/> and use **📋 Paste** or the **📄 JSON import** dialog.

No X password, session cookie, authorization token, or private X browser storage is exported. The extension observes only tweet cards rendered in the current X page.

## If the panel is missing

Check these in order:

1. Tampermonkey's global status is **Enabled**.
2. The `daggerok X Timeline Collector` toggle is green.
3. Chrome's Tampermonkey extension details have **Allow User Scripts** enabled.
4. Tampermonkey site access allows `https://x.com/*`.
5. The installed collector version is **1.1.0**.
6. Hard-reload the X tab after installing or updating the script.
7. Open Tampermonkey on the X tab and run **Show / reopen ICT Collector panel**.
8. In DevTools Console, successful execution logs:
1. The unpacked extension is enabled at `chrome://extensions`.
2. The folder loaded into Chrome directly contains `manifest.json`.
3. The extension's site access permits `https://x.com/*`.
4. You are on the X profile tab—not the dashboard—when opening the extension popup.
5. Click **Show panel**. The popup attempts a direct content-script reinjection when no receiver is detected.
6. Reload the X tab once after first installing or reloading the unpacked extension.
7. Open DevTools Console and look for:
```text
[daggerok X Collector] userscript v1.1.0 loaded
[daggerok X Collector] Chrome extension v1.0.0 loaded
```

The collector now mounts its panel before loading Tampermonkey storage, so a storage error is displayed in the panel rather than causing an invisible startup failure.

## Dashboard features

- Defaults to `@I_Am_The_ICT`, but supports other public profile handles.
Expand All @@ -64,7 +72,7 @@ Assisted scrolling is deliberately conservative:
- configurable scroll cap, defaulting to 80
- stops after repeated scans with no new rendered posts
- stops after reaching posts seen in an earlier collector run
- stores only post IDs in Tampermonkey storage to recognize previous captures
- stores only post IDs in `chrome.storage.local` to recognize previous captures

## Important limitations and Terms

Expand All @@ -81,4 +89,7 @@ Open **⚙️ Settings** in the dashboard and expand **📋 Google Sheets Setup
## Files

- [`index.html`](./index.html) — dashboard, cache, classification, filtering, CSV, and Google Sheets sync
- [`collector.user.js`](./collector.user.js) — Tampermonkey companion that observes rendered X tweet cards
- [`extension/manifest.json`](./extension/manifest.json) — Manifest V3 extension configuration
- [`extension/content.js`](./extension/content.js) — collector injected into X profile pages
- [`extension/popup.html`](./extension/popup.html) and [`popup.js`](./extension/popup.js) — toolbar controls and reinjection diagnostics
- `twitter-collector-extension.zip` — downloadable unpacked-extension package
23 changes: 23 additions & 0 deletions extension/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# daggerok X Timeline Collector — Chrome Extension

## Install

1. Download and unzip `twitter-collector-extension.zip` from the dashboard/repository.
2. Open `chrome://extensions` (or your Chromium browser's extension-management page).
3. Enable **Developer mode**.
4. Click **Load unpacked**.
5. Select the unzipped `twitter-collector-extension` folder containing `manifest.json`.
6. Pin **daggerok X Timeline Collector** to the toolbar.

## Use

1. Open <https://x.com/I_Am_The_ICT/with_replies> and log in normally.
2. Click the extension toolbar icon.
3. Click **Show panel**, **Start Manual**, or **Start Auto**.
4. Keep the X tab visible while collecting.
5. Use **Copy New JSON** in either the extension popup or floating panel.
6. Open <https://daggerok.github.io/twitter/> and press the purple clipboard button.

The popup automatically injects `content.js` if X's single-page navigation or browser startup prevented the manifest content script from loading.

No password, session cookie, authorization token, or private X storage is exported.
105 changes: 70 additions & 35 deletions collector.user.js → extension/content.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,12 @@
// ==UserScript==
// @name daggerok X Timeline Collector
// @namespace https://daggerok.github.io/twitter/
// @version 1.1.0
// @description Collect tweet cards rendered while you browse a public X profile; exports JSON for daggerok/twitter.
// @author daggerok
// @homepageURL https://daggerok.github.io/twitter/
// @supportURL https://github.com/daggerok/twitter/issues
// @updateURL https://daggerok.github.io/twitter/collector.user.js
// @downloadURL https://daggerok.github.io/twitter/collector.user.js
// @match https://x.com/*
// @match https://twitter.com/*
// @run-at document-idle
// @noframes
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @grant GM_setClipboard
// @grant GM_registerMenuCommand
// ==/UserScript==

(function () {
(() => {
'use strict';

if (globalThis.__DAGGEROK_X_COLLECTOR_EXTENSION_LOADED__) {
globalThis.__DAGGEROK_X_COLLECTOR_EXTENSION_API__?.showPanel('Panel reopened after reinjection');
return;
}
globalThis.__DAGGEROK_X_COLLECTOR_EXTENSION_LOADED__ = true;

const SCHEMA = 'daggerok-x-collector/v1';
const DEFAULT_MAX_SCROLLS = 80;
const DEFAULT_DELAY_MS = 2500;
Expand All @@ -42,12 +27,22 @@
let lastReason = 'Ready';
let ui = null;

const gmGet = async (key, fallback) => typeof GM_getValue === 'function' ? await Promise.resolve(GM_getValue(key, fallback)) : fallback;
const gmSet = async (key, value) => { if (typeof GM_setValue === 'function') await Promise.resolve(GM_setValue(key, value)); };
const storageGet = (key, fallback) => new Promise(resolve => {
chrome.storage.local.get([key], result => {
if (chrome.runtime.lastError) return resolve(fallback);
resolve(result[key] ?? fallback);
});
});
const storageSet = (key, value) => new Promise(resolve => {
chrome.storage.local.set({ [key]: value }, () => resolve());
});
const storageRemove = key => new Promise(resolve => {
chrome.storage.local.remove(key, () => resolve());
});
const knownKey = () => `known:${(targetHandle || 'unknown').toLowerCase()}`;

console.info('[daggerok X Collector] userscript v1.1.0 loaded', location.href);
document.documentElement.dataset.daggerokXCollector = '1.1.0';
console.info('[daggerok X Collector] Chrome extension v1.0.0 loaded', location.href);
document.documentElement.dataset.daggerokXCollectorExtension = '1.0.0';

function getProfileHandle() {
const part = location.pathname.split('/').filter(Boolean)[0] || '';
Expand Down Expand Up @@ -141,14 +136,14 @@
}

async function loadKnown() {
const values = await gmGet(knownKey(), []);
const values = await storageGet(knownKey(), []);
knownIds = new Set(Array.isArray(values) ? values.map(String) : []);
initialKnownIds = new Set(knownIds);
}

async function persistKnown() {
const values = Array.from(knownIds);
await gmSet(knownKey(), values.slice(Math.max(0, values.length - MAX_KNOWN_IDS)));
await storageSet(knownKey(), values.slice(Math.max(0, values.length - MAX_KNOWN_IDS)));
}

async function scanVisible() {
Expand Down Expand Up @@ -251,8 +246,17 @@
return;
}
const json = JSON.stringify(payload(), null, 2);
if (typeof GM_setClipboard === 'function') GM_setClipboard(json, 'text');
else await navigator.clipboard.writeText(json);
try {
await navigator.clipboard.writeText(json);
} catch {
const textarea = document.createElement('textarea');
textarea.value = json;
textarea.style.cssText = 'position:fixed;left:-9999px;top:-9999px';
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
textarea.remove();
}
lastReason = `Copied ${session.size} new post${session.size === 1 ? '' : 's'} as JSON`;
updateUi();
}
Expand Down Expand Up @@ -281,7 +285,7 @@
session.clear();
knownIds.clear();
initialKnownIds.clear();
if (typeof GM_deleteValue === 'function') await Promise.resolve(GM_deleteValue(knownKey()));
await storageRemove(knownKey());
updateUi();
}

Expand Down Expand Up @@ -387,12 +391,43 @@
updateUi();
}

if (typeof GM_registerMenuCommand === 'function') {
GM_registerMenuCommand('Show / reopen ICT Collector panel', () => showPanel('Panel reopened from Tampermonkey'));
GM_registerMenuCommand('Start manual collection', async () => { await showPanel('Starting manual collection'); await start(false); });
GM_registerMenuCommand('Start assisted auto-scroll', async () => { await showPanel('Starting assisted scrolling'); await start(true); });
function collectorStatus() {
return {
ok: true,
version: '1.0.0',
handle: targetHandle,
collecting,
autoScrolling,
captured: session.size,
scrollCount,
known: initialKnownIds.size,
reason: lastReason,
panelVisible: Boolean(document.getElementById(PANEL_ID))
};
}

globalThis.__DAGGEROK_X_COLLECTOR_EXTENSION_API__ = { showPanel, start, stop, copyJson, downloadJson, status: collectorStatus };

chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
if (!message || message.source !== 'daggerok-x-collector-popup') return false;
(async () => {
if (message.command === 'show') await showPanel('Panel opened from extension');
else if (message.command === 'start-manual') { await showPanel('Starting manual collection'); await start(false); }
else if (message.command === 'start-auto') { await showPanel('Starting assisted scrolling'); await start(true); }
else if (message.command === 'pause') stop('Paused from extension');
else if (message.command === 'copy') await copyJson();
else if (message.command === 'get-json') {
const status = collectorStatus();
status.json = session.size ? JSON.stringify(payload(), null, 2) : '';
if (!status.json) status.error = 'No newly captured posts are available to copy yet.';
sendResponse(status);
return;
}
sendResponse(collectorStatus());
})().catch(error => sendResponse({ ok: false, error: error?.message || String(error) }));
return true;
});

// Mount the UI first so a storage permission/error cannot make the collector fail invisibly.
createPanel();
loadKnown().then(() => {
Expand Down
33 changes: 33 additions & 0 deletions extension/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"manifest_version": 3,
"name": "daggerok X Timeline Collector",
"version": "1.0.0",
"description": "Collect tweet cards rendered in your logged-in X profile timeline and export JSON to daggerok/twitter.",
"minimum_chrome_version": "102",
"permissions": [
"activeTab",
"clipboardWrite",
"scripting",
"storage"
],
"host_permissions": [
"https://x.com/*",
"https://twitter.com/*"
],
"action": {
"default_title": "X Timeline Collector",
"default_popup": "popup.html"
},
"content_scripts": [
{
"matches": [
"https://x.com/*",
"https://twitter.com/*"
],
"js": [
"content.js"
],
"run_at": "document_idle"
}
]
}
24 changes: 24 additions & 0 deletions extension/popup.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>X Timeline Collector</title>
<style>
*{box-sizing:border-box}body{width:330px;margin:0;background:#020617;color:#e2e8f0;font:12px/1.4 Inter,system-ui,sans-serif}header{padding:12px 14px;background:#0f172a;border-bottom:1px solid #334155}h1{font-size:14px;color:#34d399;margin:0}.version{font-size:9px;color:#64748b;margin-top:2px}main{padding:12px;display:grid;gap:9px}.status{padding:10px;border:1px solid #334155;border-radius:9px;background:#0b1220;min-height:54px}.status strong{display:block;color:#a7f3d0}.meta{font-size:10px;color:#94a3b8;margin-top:3px}.row{display:flex;gap:7px}.row>*{flex:1}button,a{display:flex;align-items:center;justify-content:center;min-height:34px;border:1px solid #334155;border-radius:8px;background:#1e293b;color:#e2e8f0;text-decoration:none;font:inherit;font-weight:650;cursor:pointer}button:hover,a:hover{background:#334155}.primary{background:#059669;border-color:#10b981;color:#fff}.primary:hover{background:#047857}.purple{background:#6d28d9;border-color:#8b5cf6}.purple:hover{background:#5b21b6}.warning{color:#fbbf24;font-size:10px}.error{color:#fda4af}footer{padding:9px 12px;border-top:1px solid #1e293b;color:#64748b;font-size:9px}
</style>
</head>
<body>
<header><h1>𝕏 Timeline Collector</h1><div class="version">Chrome extension v1.0.0</div></header>
<main>
<div id="status" class="status"><strong>Checking current tab…</strong><div class="meta">Open an X profile page.</div></div>
<div class="row"><button id="show" class="primary">Show panel</button><button id="manual">Start Manual</button></div>
<div class="row"><button id="auto" class="primary">Start Auto</button><button id="pause">Pause</button></div>
<div class="row"><button id="copy" class="purple">Copy New JSON</button><a id="openX" href="https://x.com/I_Am_The_ICT/with_replies" target="_blank">Open ICT</a></div>
<a id="dashboard" href="https://daggerok.github.io/twitter/" target="_blank">Open dashboard</a>
<div class="warning">Assisted scrolling may trigger X anti-automation limits. Keep the X tab visible and pause if X warns or rate-limits you.</div>
</main>
<footer>No passwords, cookies, or X authorization tokens are exported.</footer>
<script src="popup.js"></script>
</body>
</html>
Loading