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
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,17 @@ TWITTER_CHROME_PROFILE="Profile 2" twitter feed
TWITTER_BROWSER=chrome twitter feed # Supported: arc, chrome, edge, firefox, brave
```

**Custom Chromium profile:** For ungoogled-chromium or another Chromium installation
started with `--user-data-dir`, point twitter-cli at that directory:

```bash
TWITTER_CHROMIUM_USER_DATA_DIR="/path/to/user-data-dir" twitter feed
```

The path may also point directly to a profile directory. Set
`TWITTER_CHROME_PROFILE="Profile 2"` as well to select one profile below a
User Data root.

After loading cookies, the CLI performs lightweight verification. Commands that require account access fail fast on clear auth errors (`401/403`).

### Proxy Support
Expand Down Expand Up @@ -279,6 +290,7 @@ Mode behavior:

- `No Twitter cookies found`
- Ensure you are logged in to `x.com` in a supported browser (Arc/Chrome/Edge/Firefox/Brave).
- For a custom Chromium profile, set `TWITTER_CHROMIUM_USER_DATA_DIR` to its `--user-data-dir`.
- Or set `TWITTER_AUTH_TOKEN` and `TWITTER_CT0` manually.
- Run with `-v` to see browser extraction diagnostics.

Expand Down Expand Up @@ -513,6 +525,16 @@ TWITTER_CHROME_PROFILE="Profile 2" twitter feed
TWITTER_BROWSER=chrome twitter feed # 支持: arc, chrome, edge, firefox, brave
```

**自定义 Chromium Profile**:如果使用 ungoogled-chromium,或通过
`--user-data-dir` 启动其他 Chromium,可直接指定该目录:

```bash
TWITTER_CHROMIUM_USER_DATA_DIR="/path/to/user-data-dir" twitter feed
```

该路径也可以直接指向某个 Profile 目录;若它指向 User Data 根目录,可再用
`TWITTER_CHROME_PROFILE="Profile 2"` 选择其中一个 Profile。

### 代理支持

设置 `TWITTER_PROXY` 环境变量即可:
Expand Down Expand Up @@ -551,7 +573,7 @@ score = likes_w * likes

### 常见问题

- 报错 `No Twitter cookies found`:请先登录 `x.com`,并确认浏览器为 Arc/Chrome/Edge/Firefox/Brave 之一,或手动设置环境变量
- 报错 `No Twitter cookies found`:请先登录 `x.com`,并确认浏览器为 Arc/Chrome/Edge/Firefox/Brave 之一;自定义 Chromium 可将 `TWITTER_CHROMIUM_USER_DATA_DIR` 指向 `--user-data-dir`,或手动设置认证环境变量
- 如需查看浏览器提取细节,可加 `-v` 打开诊断日志。
- 报错 `Cookie expired or invalid`:Cookie 过期,重新登录后重试。
- 报错 `Unable to get key for cookie decryption`(macOS Keychain 问题):
Expand Down
69 changes: 68 additions & 1 deletion tests/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,16 @@ def test_load_from_env_logs_incomplete_env(monkeypatch, caplog) -> None:
assert "Environment cookies incomplete" in caplog.text


def test_get_browser_order_prepends_custom_chromium_dir(monkeypatch, tmp_path) -> None:
monkeypatch.delenv("TWITTER_BROWSER", raising=False)
monkeypatch.setenv("TWITTER_CHROMIUM_USER_DATA_DIR", str(tmp_path))

order = auth._get_browser_order()

assert order[0] == "custom-chromium"
assert "chrome" in order


def test_extract_cookies_from_jar_logs_missing_required_cookies(caplog) -> None:
class Cookie:
def __init__(self, domain: str, name: str, value: str) -> None:
Expand Down Expand Up @@ -135,7 +145,9 @@ def _run(cmd, capture_output=True, text=True, timeout=15):
cookies, diagnostics = auth._extract_via_subprocess()

assert cookies is None
assert '"arc": browser_cookie3.arc' in seen["script"]
assert 'DEFAULT_ORDER = ["arc", "chrome", "edge", "firefox", "brave"]' in seen["script"]
assert 'CUSTOM_CHROMIUM_BROWSER = "custom-chromium"' in seen["script"]
assert "browser_cookie3.chromium(" in seen["script"]


def test_extract_via_subprocess_retries_uv_when_current_env_has_no_output(monkeypatch) -> None:
Expand Down Expand Up @@ -243,6 +255,25 @@ def test_iter_chrome_cookie_files_env_override(monkeypatch, tmp_path) -> None:
assert "Profile 5" in paths[0]


def test_iter_chrome_cookie_files_supports_custom_user_data_dir(monkeypatch, tmp_path) -> None:
root = tmp_path / "Ungoogled Chromium"
network_dir = root / "Profile 1" / "Network"
network_dir.mkdir(parents=True)
cookie_file = network_dir / "Cookies"
cookie_file.touch()
local_state = root / "Local State"
local_state.touch()

monkeypatch.setenv("TWITTER_CHROMIUM_USER_DATA_DIR", str(root))
monkeypatch.delenv("TWITTER_CHROME_PROFILE", raising=False)

paths = auth._iter_chrome_cookie_files("custom-chromium")

assert paths == [str(cookie_file)]
assert auth._profile_name_from_cookie_file(str(cookie_file)) == "Profile 1"
assert auth._chromium_key_file_for_cookie(str(cookie_file)) == str(local_state)


def test_iter_chrome_cookie_files_edge_linux_uses_microsoft_edge_path(monkeypatch, tmp_path) -> None:
monkeypatch.setattr(auth.sys, "platform", "linux")
edge_dir = tmp_path / ".config" / "microsoft-edge"
Expand Down Expand Up @@ -322,6 +353,42 @@ def mock_arc(cookie_file=None):
assert cookies["ct0"] == "csrf456"


def test_extract_in_process_uses_chromium_loader_for_custom_dir(monkeypatch, tmp_path) -> None:
class Cookie:
def __init__(self, domain: str, name: str, value: str) -> None:
self.domain = domain
self.name = name
self.value = value

root = tmp_path / "User Data"
profile_dir = root / "Default" / "Network"
profile_dir.mkdir(parents=True)
cookie_file = profile_dir / "Cookies"
cookie_file.touch()
local_state = root / "Local State"
local_state.touch()
seen = {}

def chromium(cookie_file=None, key_file=None):
seen["cookie_file"] = cookie_file
seen["key_file"] = key_file
return [
Cookie(".x.com", "auth_token", "custom-token"),
Cookie(".x.com", "ct0", "custom-csrf"),
]

monkeypatch.setenv("TWITTER_CHROMIUM_USER_DATA_DIR", str(root))
monkeypatch.setattr(auth, "_get_browser_order", lambda: ["custom-chromium"])
monkeypatch.setitem(sys.modules, "browser_cookie3", SimpleNamespace(chromium=chromium))

cookies, diagnostics = auth._extract_in_process()

assert diagnostics == []
assert cookies is not None
assert cookies["auth_token"] == "custom-token"
assert seen == {"cookie_file": str(cookie_file), "key_file": str(local_state)}


def test_diagnose_keychain_issues_detects_decryption_error(monkeypatch) -> None:
"""_diagnose_keychain_issues should detect Keychain-related error strings."""
monkeypatch.setattr("sys.platform", "darwin")
Expand Down
Loading
Loading