Skill: alexander10011/twitter-query (version 1.0.0)

--- name: twitter-query description: >- Query X/Twitter via twitterapi.io read-only APIs by account (user timeline) or by keyword (advanced search). Outputs structured JSON; no LLM, no trend scoring. Use when the user asks for tweets from a handle, user timeline, keyword/hashtag/cashtag search, or 推特/X 推文查询. --- # Twitter / X 推文查询（只读） 通过 [twitterapi.io](https://docs.twitterapi.io/introduction) 拉取推文：**按账号**或**按关键词**，输出 **JSON**。不集成 LLM，不做趋势榜/热度建模。 ## 环境 - `TWITTER_API_KEY`：必填（HTTP Header `X-API-Key`）。 - `TWITTER_API_BASE`：可选，默认 `https://api.twitterapi.io`。 ## OpenClaw / ClawHub 安装 在已支持 Skills 的客户端中（以仓库发布名为准，示例为 `alexander10011/twitter-query`）： ```bash npx skills add alexander10011/twitter-query ``` 安装后，在**技能包根目录**下执行脚本（路径以实际克隆位置为准）。 ## 脚本路径（仓库根目录） | 能力 | 命令 | |------|------| | 某用户时间线 | `python3 scripts/query_by_user.py USERNAME [选项]` | | 关键词高级搜索 | `python3 scripts/query_by_keyword.py "查询字符串" [选项]` | ### 按账号 ```bash export TWITTER_API_KEY="你的key" python3 scripts/query_by_user.py VitalikButerin --max-pages 5 python3 scripts/query_by_user.py someuser --include-replies --max-pages 10 ``` - 接口：`GET /twitter/user/last_tweets`（[文档](https://docs.twitterapi.io/api-reference/endpoint/get_user_last_tweets)），`cursor` 分页，每页最多约 20 条。 - 时间范围由分页量间接限制；需要「近 N 天」可对返回的 `createdAt` 再过滤。 ### 按关键词 ```bash python3 scripts/query_by_keyword.py '$BTC min_faves:5' --query-type Latest --max-pages 3 python3 scripts/query_by_keyword.py 'from:elonmusk since:2026-03-01_00:00:00_UTC' --query-type Top ``` - 接口：`GET /twitter/tweet/advanced_search`（[文档](https://docs.twitterapi.io/api-reference/endpoint/tweet_advanced_search)）。 - `queryType`：`Latest` 或 `Top`；默认 `Latest`。 - 语法参考：[twitter-advanced-search](https://github.com/igorbrigadir/twitter-advanced-search)。 ## 输出 脚本向 **stdout** 打印 JSON（`utf-8`），含 `meta` 与 `tweets`。 ## Agent 工作方式 1. 确认已设置 `TWITTER_API_KEY`。 2. 选择 `query_by_user` 或 `query_by_keyword`，运行脚本。 3. 若需中文摘要或观点归纳，在**当前对话模型内**完成，本 Skill 不调用外部总结 API。 ## 许可证 MIT，见 [LICENSE](LICENSE)。

path: PUBLISH.md
content: # 发布到 GitHub 与 ClawHub（维护者）

我无法替你完成「在网站上点提交」这一步；按下列清单操作即可。

## 1. 独立 Git 仓库（推荐）

把本目录作为 **仓库根目录**（不要多包一层文件夹）：

```text
twitter-query/
├── SKILL.md
├── README.md
├── LICENSE
├── claw.json
├── clawhub.json
├── PUBLISH.md
├── .gitignore
└── scripts/
    ├── query_by_user.py
    └── query_by_keyword.py
```

在 GitHub 新建仓库，例如 `twitter-query`，推送 `main`。

## 2. 替换作者与链接（若你不是 `alexander10011`）

全局替换：

- `alexander10011` → 你的 GitHub 用户名或组织名
- `Alex Wang` → 你的名字（`LICENSE` / `claw.json` / `clawhub.json`）

涉及文件：`README.md`（徽章与 `npx` 示例）、`claw.json`、`clawhub.json`、`LICENSE`（可选）。

## 3. ClawHub

1. 打开 [ClawHub](https://clawhub.ai)，使用当前平台要求的流程：**关联 GitHub**、**新建 Skill** 或 **从仓库导入**（以 ClawHub 当时文档为准）。
2. 确保 listing 的 **slug** 与 `npx skills add <owner>/twitter-query` 一致。
3. 上架后把 README 里徽章链接改成你的真实页面，例如：  
   `https://clawhub.ai/<你的owner>/twitter-query`

## 4. 验证

```bash
npx skills add <owner>/twitter-query
export TWITTER_API_KEY="..."
python3 scripts/query_by_user.py elonmusk --max-pages 1
```

## 5. 合规与扫描

- Skill 仅向 **twitterapi.io** 发 HTTPS 请求；不在仓库内收集用户数据。
- 若静态扫描误报，可引用 `claw.json` 中的 `privacy.externalServices` 说明，必要时附 OpenClaw 安全扫描结论（参考你其他 Skill 的申诉材料写法）。


path: README.md
content: # Twitter Query

**Query X (Twitter) by account or keyword** via [twitterapi.io](https://docs.twitterapi.io/introduction). **JSON output**, Python stdlib only, **no bundled LLM**.

[![ClawHub](https://img.shields.io/badge/ClawHub-twitter--query-blue)](https://clawhub.ai/alexander10011/twitter-query)
[![License](https://img.shields.io/badge/License-MIT-green)](LICENSE)

---

## Features

| Mode | API | Script |
|------|-----|--------|
| User timeline | `GET /twitter/user/last_tweets` | `scripts/query_by_user.py` |
| Keyword search | `GET /twitter/tweet/advanced_search` | `scripts/query_by_keyword.py` |

- Cursor pagination (`cursor` / `has_next_page`)
- `queryType`: `Latest` or `Top` for advanced search
- Retries on HTTP 429 with backoff

## Requirements

- **Python 3.10+** (uses `from __future__ import annotations`; stdlib only)
- **twitterapi.io API key** → set `TWITTER_API_KEY`
- Optional: `TWITTER_API_BASE` (default `https://api.twitterapi.io`)

## Install (OpenClaw / ClawHub)

```bash
npx skills add alexander10011/twitter-query
```

> If you fork under another GitHub user, replace `alexander10011` with your username in this command and in `claw.json` / `clawhub.json` / badge URLs.

## Quick start

```bash
export TWITTER_API_KEY="your_key_here"
cd /path/to/twitter-query   # skill root after install

python3 scripts/query_by_user.py VitalikButerin --max-pages 3
python3 scripts/query_by_keyword.py '"$BTC" min_faves:2' --query-type Latest --max-pages 2
```

## Privacy

- Your **API key** is sent only to **twitterapi.io** in the `X-API-Key` header.
- **Queries and usernames** are sent as URL parameters to that API.
- This repo does not phone home or collect telemetry.

See `claw.json` → `privacy.externalServices` for OpenClaw disclosure.

## Publish checklist (maintainer)

1. Create GitHub repo `twitter-query` under your account (default links assume `alexander10011/twitter-query`).
2. Push this folder as repo root (`SKILL.md`, `scripts/`, `claw.json`, `clawhub.json`, `LICENSE`, `README.md`).
3. On [ClawHub](https://clawhub.ai), submit or sync the skill per current ClawHub docs (CLI or web).
4. After listing is live, confirm badge URL `https://clawhub.ai/<owner>/twitter-query`.

维护者发布步骤见本仓库 [PUBLISH.md](PUBLISH.md)。若本目录放在更大 monorepo 中，另有一份汇总说明：`docs/twitter-query-release-summary.md`（仅在该 monorepo 中存在）。

## License

MIT — see [LICENSE](LICENSE).


path: claw.json
content: {
  "name": "twitter-query",
  "version": "1.0.0",
  "description": "Query X/Twitter by account (user timeline) or keyword (advanced search) via twitterapi.io. Returns JSON. Requires user-supplied TWITTER_API_KEY. No LLM, no trend scoring.",
  "author": "Alex Wang",
  "license": "MIT",
  "permissions": ["network"],
  "entryPoint": "SKILL.md",
  "tags": [
    "twitter",
    "x",
    "social",
    "search",
    "tweets",
    "timeline",
    "openclaw"
  ],
  "repository": "https://github.com/alexander10011/twitter-query",
  "homepage": "https://github.com/alexander10011/twitter-query",
  "privacy": {
    "dataCollection": [],
    "externalServices": [
      {
        "name": "twitterapi.io",
        "url": "https://api.twitterapi.io",
        "purpose": "Read-only Twitter/X data: user last tweets and advanced tweet search (third-party API; user provides own API key).",
        "dataShared": [
          "search_queries",
          "usernames_requested",
          "api_key_in_http_header_x_api_key"
        ]
      }
    ],
    "localStorage": []
  }
}


path: clawhub.json
content: {
  "name": "twitter-query",
  "version": "1.0.0",
  "description": "Query X/Twitter by account or keyword via twitterapi.io. JSON output. You must supply your own TWITTER_API_KEY. No LLM bundled.",
  "longDescription": "Read-only OpenClaw skill for X (Twitter):\n\n- **By account**: `GET /twitter/user/last_tweets` with cursor pagination.\n- **By keyword**: `GET /twitter/tweet/advanced_search` with `queryType` Latest or Top.\n\n**Requirements**: Set environment variable `TWITTER_API_KEY` (from [twitterapi.io](https://docs.twitterapi.io/introduction)). Optional `TWITTER_API_BASE` defaults to `https://api.twitterapi.io`.\n\n**Privacy**: Your API key is sent only to twitterapi.io in the `X-API-Key` header. Queries and usernames are sent as request parameters. No analytics or telemetry in this repo.\n\n**No LLM**: Summarize in your agent if needed. Python stdlib only.\n\nDocs: [Advanced search syntax](https://github.com/igorbrigadir/twitter-advanced-search).",
  "author": "Alex Wang",
  "license": "MIT",
  "repository": "https://github.com/alexander10011/twitter-query",
  "homepage": "https://github.com/alexander10011/twitter-query",
  "keywords": [
    "twitter",
    "x",
    "tweets",
    "search",
    "timeline",
    "social",
    "twitterapi",
    "openclaw"
  ]
}


path: scripts/query_by_keyword.py
content: #!/usr/bin/env python3
"""
Search tweets via twitterapi.io GET /twitter/tweet/advanced_search.
Env: TWITTER_API_KEY (required), TWITTER_API_BASE (optional).
"""
from __future__ import annotations

import argparse
import json
import os
import sys
import time
import urllib.error
import urllib.parse
import urllib.request

DEFAULT_BASE = "https://api.twitterapi.io"
MAX_RETRIES = 3
RETRY_DELAY = 2.0


def _request(url: str, api_key: str) -> dict:
    req = urllib.request.Request(
        url,
        headers={
            "X-API-Key": api_key,
            "User-Agent": "twitter-query-skill/1.0",
        },
        method="GET",
    )
    last_err: Exception | None = None
    for attempt in range(MAX_RETRIES):
        try:
            with urllib.request.urlopen(req, timeout=30) as resp:
                body = resp.read().decode("utf-8")
                return json.loads(body)
        except urllib.error.HTTPError as e:
            last_err = e
            if e.code == 429 and attempt < MAX_RETRIES - 1:
                time.sleep(RETRY_DELAY * (attempt + 1))
                continue
            try:
                detail = e.read().decode("utf-8")
            except Exception:
                detail = ""
            raise SystemExit(f"HTTP {e.code}: {detail or e.reason}") from e
        except Exception as e:
            last_err = e
            if attempt < MAX_RETRIES - 1:
                time.sleep(RETRY_DELAY)
                continue
            raise
    raise SystemExit(str(last_err))


def _tweet_url(author: str | None, tweet_id: str | None) -> str | None:
    if author and tweet_id:
        return f"https://x.com/{author}/status/{tweet_id}"
    return None


def normalize_tweet(raw: dict) -> dict:
    author = (raw.get("author") or {}) if isinstance(raw.get("author"), dict) else {}
    un = author.get("userName") or author.get("username")
    tid = raw.get("id")
    return {
        "id": tid,
        "url": raw.get("url") or _tweet_url(un, str(tid) if tid is not None else None),
        "text": raw.get("text"),
        "createdAt": raw.get("createdAt"),
        "lang": raw.get("lang"),
        "likeCount": raw.get("likeCount"),
        "retweetCount": raw.get("retweetCount"),
        "replyCount": raw.get("replyCount"),
        "quoteCount": raw.get("quoteCount"),
        "viewCount": raw.get("viewCount"),
        "author": {
            "userName": un,
            "name": author.get("name"),
            "followers": author.get("followers"),
            "isBlueVerified": author.get("isBlueVerified"),
        },
    }


def main() -> None:
    p = argparse.ArgumentParser(description="Query tweets by advanced search query.")
    p.add_argument("query", help="Twitter advanced search query string")
    p.add_argument(
        "--query-type",
        choices=("Latest", "Top"),
        default="Latest",
        help="Latest or Top (see API docs). Default Latest.",
    )
    p.add_argument(
        "--max-pages",
        type=int,
        default=5,
        help="Max cursor pages (~20 tweets per page). Default 5.",
    )
    args = p.parse_args()

    [REDACTED_SECRET]"TWITTER_API_KEY", "").strip()
    if not [REDACTED_SECRET]"Missing TWITTER_API_KEY", file=sys.stderr)
        sys.exit(1)

    base = os.environ.get("TWITTER_API_BASE", DEFAULT_BASE).rstrip("/")

    all_raw: list[dict] = []
    cursor = ""
    pages = 0
    meta_pages: list[dict] = []

    while pages < args.max_pages:
        q = urllib.parse.urlencode(
            {
                "query": args.query,
                "queryType": args.query_type,
                "cursor": cursor,
            }
        )
        url = f"{base}/twitter/tweet/advanced_search?{q}"
        data = _request(url, api_key)
        batch = data.get("tweets") or []
        all_raw.extend(batch)
        meta_pages.append(
            {
                "page": pages + 1,
                "count": len(batch),
                "has_next_page": data.get("has_next_page"),
            }
        )
        pages += 1
        if not data.get("has_next_page"):
            break
        cursor = data.get("next_cursor") or ""
        if not cursor:
            break

    out = {
        "meta": {
            "endpoint": "advanced_search",
            "query": args.query,
            "queryType": args.query_type,
            "pages_fetched": pages,
            "tweet_count": len(all_raw),
            "pages": meta_pages,
        },
        "tweets": [normalize_tweet(t) for t in all_raw],
    }
    print(json.dumps(out, ensure_ascii=False, indent=2))


if __name__ == "__main__":
    main()


path: scripts/query_by_user.py
content: #!/usr/bin/env python3
"""
Fetch tweets for one X/Twitter user via twitterapi.io GET /twitter/user/last_tweets.
Env: TWITTER_API_KEY (required), TWITTER_API_BASE (optional).
"""
from __future__ import annotations

import argparse
import json
import os
import sys
import time
import urllib.error
import urllib.parse
import urllib.request

DEFAULT_BASE = "https://api.twitterapi.io"
MAX_RETRIES = 3
RETRY_DELAY = 2.0


def _request(url: str, api_key: str) -> dict:
    req = urllib.request.Request(
        url,
        headers={
            "X-API-Key": api_key,
            "User-Agent": "twitter-query-skill/1.0",
        },
        method="GET",
    )
    last_err: Exception | None = None
    for attempt in range(MAX_RETRIES):
        try:
            with urllib.request.urlopen(req, timeout=30) as resp:
                body = resp.read().decode("utf-8")
                return json.loads(body)
        except urllib.error.HTTPError as e:
            last_err = e
            if e.code == 429 and attempt < MAX_RETRIES - 1:
                time.sleep(RETRY_DELAY * (attempt + 1))
                continue
            try:
                detail = e.read().decode("utf-8")
            except Exception:
                detail = ""
            raise SystemExit(f"HTTP {e.code}: {detail or e.reason}") from e
        except Exception as e:
            last_err = e
            if attempt < MAX_RETRIES - 1:
                time.sleep(RETRY_DELAY)
                continue
            raise
    raise SystemExit(str(last_err))


def _tweet_url(author: str | None, tweet_id: str | None) -> str | None:
    if author and tweet_id:
        return f"https://x.com/{author}/status/{tweet_id}"
    return None


def normalize_tweet(raw: dict) -> dict:
    author = (raw.get("author") or {}) if isinstance(raw.get("author"), dict) else {}
    un = author.get("userName") or author.get("username")
    tid = raw.get("id")
    out = {
        "id": tid,
        "url": raw.get("url") or _tweet_url(un, str(tid) if tid is not None else None),
        "text": raw.get("text"),
        "createdAt": raw.get("createdAt"),
        "lang": raw.get("lang"),
        "likeCount": raw.get("likeCount"),
        "retweetCount": raw.get("retweetCount"),
        "replyCount": raw.get("replyCount"),
        "quoteCount": raw.get("quoteCount"),
        "viewCount": raw.get("viewCount"),
        "isReply": raw.get("isReply"),
        "author": {
            "userName": un,
            "name": author.get("name"),
            "followers": author.get("followers"),
            "isBlueVerified": author.get("isBlueVerified"),
        },
    }
    return out


def main() -> None:
    p = argparse.ArgumentParser(description="Query tweets by X username (last_tweets API).")
    p.add_argument("userName", help="Screen name without @")
    p.add_argument(
        "--max-pages",
        type=int,
        default=5,
        help="Max cursor pages (each page up to ~20 tweets). Default 5.",
    )
    p.add_argument(
        "--include-replies",
        action="store_true",
        help="Include reply tweets (default excludes per API default).",
    )
    args = p.parse_args()

    [REDACTED_SECRET]"TWITTER_API_KEY", "").strip()
    if not [REDACTED_SECRET]"Missing TWITTER_API_KEY", file=sys.stderr)
        sys.exit(1)

    base = os.environ.get("TWITTER_API_BASE", DEFAULT_BASE).rstrip("/")
    user = args.userName.lstrip("@")

    all_raw: list[dict] = []
    cursor = ""
    pages = 0
    meta_pages: list[dict] = []

    while pages < args.max_pages:
        q = urllib.parse.urlencode(
            {
                "userName": user,
                "cursor": cursor,
                "includeReplies": "true" if args.include_replies else "false",
            }
        )
        url = f"{base}/twitter/user/last_tweets?{q}"
        data = _request(url, api_key)
        status = data.get("status")
        if status and status != "success":
            msg = data.get("message") or data.get("msg") or "unknown error"
            print(json.dumps({"meta": {"error": True, "message": msg, "raw": data}}, ensure_ascii=False))
            sys.exit(1)

        batch = data.get("tweets") or []
        all_raw.extend(batch)
        meta_pages.append(
            {
                "page": pages + 1,
                "count": len(batch),
                "has_next_page": data.get("has_next_page"),
            }
        )
        pages += 1
        if not data.get("has_next_page"):
            break
        cursor = data.get("next_cursor") or ""
        if not cursor:
            break

    out = {
        "meta": {
            "endpoint": "last_tweets",
            "userName": user,
            "includeReplies": args.include_replies,
            "pages_fetched": pages,
            "tweet_count": len(all_raw),
            "pages": meta_pages,
        },
        "tweets": [normalize_tweet(t) for t in all_raw],
    }
    print(json.dumps(out, ensure_ascii=False, indent=2))


if __name__ == "__main__":
    main()


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)