Twitterの特定のユーザーの画像を全てDLするだけ

## 何を作ったか

友達に依頼されてTwitterの特定ユーザーのアップロードしている画像をまとめてダウンロードするプログラムを書いてみた。

流行りに乗ってdenoを使ってみた。

## できたもの

<details>

<summary>クリックして表示</summary><div>

汚いコードでごめんなさい

```ts
import { download } from 'https://deno.land/x/download@v1.0.1/mod.ts';

const YOUR_BEARER_TOKEN = 'YOUR_BEARER_TOKEN';

const excludeNullFromKey = (obj: Record<string, string | undefined>) => {
  const newObj: Record<string, string> = {};
  Object.keys(obj).forEach((key) => {
    const value = obj[key];
    if (value != undefined) {
      newObj[key] = value;
    }
  });
  return newObj;
};

const getUserIdFromScreenName = async (screen_name: string) => {
  const res = await fetch(
    `https://api.twitter.com/2/users/by/username/${screen_name}`,
    {
      headers: {
        Authorization: 'Bearer ' + YOUR_BEARER_TOKEN,
      },
    }
  );
  const json = await res.json();
  return json.data.id;
};

const getImagesFromId = async (
  id: string,
  next?: string
): Promise<string[]> => {
  const params = excludeNullFromKey({
    max_results: '100',
    exclude: 'retweets',
    expansions: 'attachments.media_keys',
    pagination_token: next,
    'media.fields': 'url',
    'tweet.fields': 'attachments',
  });

  const raw_response = await fetch(
    `https://api.twitter.com/2/users/${id}/tweets?` +
      new URLSearchParams(params),
    {
      headers: {
        Authorization: 'Bearer ' + YOUR_BEARER_TOKEN,
      },
    }
  );
  const json_response = await raw_response.json();

  const images: string[] = json_response.includes.media.map(
    (item: { url: string }) => item.url
  );

  if (json_response.meta.next_token !== undefined) {
    const next_images = await getImagesFromId(
      id,
      json_response.meta.next_token
    );
    return [...images, ...next_images];
  }

  return images;
};

const downloadImages = async (images: string[]) => {
  for (let i = 0; i < images.length; i++) {
    const image = images[i];
    if (image !== undefined) {
      const file = image.split('/').pop();
      await download(image, { file, dir: './images/' });

      const progress = ((i + 1) / images.length) * 100;
      const progressCount = 20;
      Deno.stdout.write(
        new TextEncoder().encode(
          `${
            '#'.repeat(progress / 100 * progressCount) +
            ' '.repeat(progressCount - progress / 100 * progressCount)
          } ${Math.floor(progress)}%\r`
        )
      );
    }
  }
};

const userId = await getUserIdFromScreenName('SCREEN_NAME');

const userImages = await getImagesFromId(userId);

console.log(`${userImages.length} images found`);

await downloadImages(userImages);

console.log('all images downloaded');
```

</div></details>

## ハマったポイント

同じようなことをやろうとしている人のお役に立てればと

### エンドポイント探し

まずTwitterAPIのどのエンドポイントを使うのかを探すのがちょい大変だった。公式リファレンスが微妙に探しづらいので、ググってでてきたプログラムから拾ってきた。

- スクリーンネーム → ユーザーID `/2/users/by/username/:SCREEN_NAME`
- ユーザーID → ツイート一覧 `/2/users/:id/tweets`

この2つで実現できた。

### 画像のURLの取得

ツイートの取得は出来てもデフォルトだと画像がついてこないので若干書き換える必要がある。

`/2/users/:id/tweets`でツイートを取得するときのクエリパラメータに以下の2つを追加する。

- `expansions=attachments.media_keys`
- `tweet.fields=attachments`
- `media.fields=url`

そうするとレスポンスがこんな感じになる。

```json
{
    "data": [
        {
            "text": "ツイートの内容",
            "id": "ツイートのID",
            "attachments": {
                "media_keys": ["メディアキー"]
            }
        },
        ...
    ],
    "meta": {...},
    "includes": {
        "media": [
            {
                media_key: "メディアキー",
                type: "...",
                url: "画像のURL"
            },
            ...
        ]
    }
}
```

今回はツイートと紐づける必要がなかったので`includes`の中のURLを取得してダウンロードしたが、ツイートと紐づけたい場合はツイートからメディアキーを取得してくる必要がある。

### ツイートの取得を連続的にやる

レスポンスの中の`next_token`という部分を取り出して、次回のリクエスト時に`pagination_token=next_token`というクエリパラメータを設定することで連続的にツイートを取得できる。

```json
{
    "data": [...],
    ...
    "meta": {
        "next_token": "...",
        ...
    }
}
```

### 画像のダウンロード

denoのdownloadというライブラリを使った。詳しくは[公式リファレンス](https://deno.land/x/download@v1.0.1/mod.ts)を見ればわかると思うが、`download`というメソッドを使えばできる。

## 感想

TwitterAPIは微妙に使いづらい。

Denoは使いやすそうな感じはする。ただまだ環境が整ってない感じがあるので今後に期待。