twitterで推しの漫画をより楽しむためのコマンドラインツール「imgdler」を作ってみた

はじめまして、駆け出しバックエンドエンジニアの[かず](https://github.com/kazdevl)です。
何気に自身の技術記事を公開するのは初めてなので多めにみてもらえるとありがたいです。

twitterで推しの漫画をより楽しむためのコマンドラインツール「[imgdler](https://github.com/kazdevl/imgdler)」を作った話について記述します。


本記事は[CyberAgent22新卒Advent Calendar 2021](https://adventar.org/calendars/6671) 15日目の記事です。

https://adventar.org/calendars/6671


## 経緯
つい最近、開発に協力しているTwitter API v2のクライアントライブラリである[gotwtr](https://github.com/sivchari/gotwtr)がリリースされたので、宣伝も兼ねて何か作ろうな〜と考えていました。

ちょうど、twitterに関連した課題を抱えていた私には、ちょうど良い機会でした。

その課題とはtwitter漫画における**最新話の追従の難しさ**と**前話や続話の読みにくさ**です。


本記事で述べているtwitter漫画とは、twitterで公開している漫画と定義しています。
twitter漫画で有名なものとして「100日後に死ぬワニ」などがあるかなあと思います。

上記のようなtwitter漫画を読んでいると、最新話などのツイートが著者によって公開されますが、twitterはみなさんご存知の通り、ツイートが流れていきますので、なかなか最新話をキャッチアップすることが大変です。

また、好きなtwitter漫画をまとめて読みたいと考えたときに、話ごとに別れていたり、リツイート形式になっていたりと、まとめてスムーズに読むことが難しいです。

なれば、[gotwtr](https://github.com/sivchari/gotwtr)の宣伝も兼ねて、上記の**追従性**と**まとまり具合**を解決するツールでも作ってみるか！となったのが経緯です。

## ツールの概要
そんな経緯で作った「[imdler](https://github.com/kazdevl/imgdler)」ですが、大まかに以下の3つのコマンドを提供します。
1. `start`: 著者、ツイートのキーワードなどを元に、毎21時にツイートを確認して画像を取り込む
2. `list`: 現在取り込んでいる画像の著者の名前一覧を表示する
3. `open`: 指定した著者の画像一覧をhtmlファイルに取り込んで、それをブラウザで開ける

上記のコマンドによって、「[imdler](https://github.com/kazdevl/imgdler)」のユーザは、推しの漫画の最新話を追従し、かつ、いつでもまとめて漫画を読むことができます。


## 使い方
以下のtwitter漫画(※テスト用に作ったものです)を用いて、
「[imdler](https://github.com/kazdevl/imgdler)」の使い方を解説していきます。
※ 事前にPCにGoが入っていて、binにパスが通っている前提です。

https://twitter.com/_kz_dev/status/1470931721470492684?s=21

### 1. 「[imdler](https://github.com/kazdevl/imgdler)」をinstallする
```bash
$ go install github.com/kazdevl/imgdler/cmd/imgdler@latest
```

### 2. 読み込みたい漫画を定期的に取り込む
フラグについて

- aは読み込みたい漫画の著者名(twitterアカウント名)です
    - @を除いたアカウント名です
- kは読み込みたい漫画のツイートに含まれるキーワードです
    - 上記の例で言うと"テスト"や"テスト for アドベントカレンダー"になります
- tは[OAuth 2.0. Bearer tokens](https://developer.twitter.com/ja/docs/authentication/oauth-2-0)のことです
    - Twitter APIを呼び出すのに利用します

```bash
$ imgdler start -a _kz_dev -k テスト -t [your OAuth 2.0. Bearer tokens]
```
上記のコマンドによって、指定した著者の指定したキーワードを含むツイートの画像を取り込みます。

### 3. 取り込んだ画像の著者名を確認する
```bash
$ imgdler list

# output
The list of author names that you can read
[0]: _kz_dev
You cna read with `imgdler open [author name]`
```

### 4. 画像をまとめて読む
```bash
$ imgdler open _kz_dev
```
上記のコマンドでブラウザが立ち上がって、以下の様に読むことができます。
![スクリーンショット 2021-12-15 20.43.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/433436/5de66b34-83e5-8ac8-93c1-a303a1f65b25.png)
![スクリーンショット 2021-12-15 20.40.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/433436/8c29ed80-b7d8-953b-fe1e-a940d65d5f10.png)

## 実装ポイント
ファイルからコードを抜粋して説明していきます。
### 画像の取得
ここのツイート検索でgotwtrを利用しました！

```go:usecase/twitter.go
type TwitterUsecase struct {
    c *gotwtr.Client
}

func NewTwitterUsecase(token string) *TwitterUsecase {
    return &TwitterUsecase{
        c: gotwtr.New(token),
    }
}

// ここでツイートを取得して画像を抽出している
func (t *TwitterUsecase) FetchContent(author, keyword string, max int) ([]entity.Pages, error) {
    // gotwtrのSearchRecentTweetsで指定した条件でツイートを検索する
    /* 指定した条件
    * 1. 著者
    * 2. キーワードを含む
    * 3. リツイートではない
    * 4. 取得するツイート数
    * 5. 開始時間が1日前
    */
    query := fmt.Sprintf("from:%s -is:retweet \"%s\"", author, keyword)
    res, err := t.c.SearchRecentTweets(context.Background(), query, &gotwtr.SearchTweetsOption{
        Expansions:  []gotwtr.Expansion{gotwtr.ExpansionAttachmentsMediaKeys},
        MediaFields: []gotwtr.MediaField{gotwtr.MediaFieldMediaKey, gotwtr.MediaFieldURL},
        TweetFields: []gotwtr.TweetField{gotwtr.TweetFieldAttachments, gotwtr.TweetFieldCreatedAt},
        MaxResults:  max,
        StartTime:   time.Now().In(time.UTC).AddDate(0, 0, -1),
    })
    if err != nil {
        return []entity.Pages{}, err
    }
    pagesList := make([]entity.Pages, len(res.Tweets))
    for index, tweet := range res.Tweets {
        pagesList[index].Datetime, _ = time.Parse(time.RFC3339, tweet.CreatedAt)
        // gotwtrのレスポンスのツイートから画像URLリストを取得する
        pagesList[index].Links = t.getTweetImageLinks(tweet, res.Includes.Media)
    }

    return pagesList, nil
}

// 引数を使って、画像URLリストを取得する
func (t *TwitterUsecase) getTweetImageLinks(tweet *gotwtr.Tweet, media []*gotwtr.Media) []string {
    links := make([]string, 0, 4)
    for _, key := range tweet.Attachments.MediaKeys {
        for _, m := range media {
            if key == m.MediaKey {
                links = append(links, m.URL)
            }
        }
    }
    return links
}

```


### 画像の表示
```go:cmd/imgdler/open/open.go
func proccess(author, contentsDir string) error {
     // 画像が格納されているディレクトリの取得
	contentDir := filepath.Join(contentsDir, author)
     // ディレクトリ配下の情報を取得
	des, err := os.ReadDir(contentDir)
	if err != nil {
		return err
	}
　　 // 画像ファイル名リストの取得
	images := make([]string, len(des))
	for i, de := range des {
		filename := de.Name()
		fileInfo := strings.Split(filename, ".")
		if len(fileInfo) <= 1 {
			continue
		}
		if fileInfo[1] != "jpg" {
			continue
		}
		images[i] = fmt.Sprintf("%s/%s", contentDir, de.Name())
	}
    // アルファベット順で整列
	sort.Strings(images)

	// html形式のテンプレートを読み込む
	t, err := template.New("reader").Parse(tpl)
	if err != nil {
		return err
	}
    // htmlファイル作成
	f, err := os.Create(fmt.Sprintf("%s/index.html", contentDir))
	defer f.Close()
	if err != nil {
		return err
	}
    // html形式のテンプレートに画像ファイル名リストを渡してhtmlファイルにデータを書き込む
	if err := t.Execute(f, images); err != nil {
		return err
	}

	// ブラウザを立ち上げる
	if err := browser.OpenFile(fmt.Sprintf("%s/index.html", contentDir)); err != nil {
		return err
	}
	return nil
}

// htmlテンプレートを定義
const tpl = `
<!doctype html>
<html lang="ja">

<head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/css/bootstrap.min.css"
        integrity="sha384-GJzZqFGwb1QTTN6wy59ffF1BuGJpLSa9DkKMp0DgiMDm4iYMj70gZWKYbI706tWS" crossorigin="anonymous">
    <title>imgdler</title>
</head>

<body class="bg-secondary">
    <header class="fixed-top mb-3 bg-dark" style="opacity: 0.5;" onMouseOut="this.style.opacity=0"
        onMouseOver="this.style.opacity=1">
        <nav class="navbar navbar-dark w-75 mr-auto ml-auto">
            <div class="container-fluid">
                <span class="navbar-brand">
                    imgdler
                </span>
            </div>
        </nav>
    </header>
    <main class="w-50 mr-auto ml-auto">
        <div class="bg-light w-100 text-center">
            {{range .}}
            <img src="{{.}}" class="img-fluid mb-1">
            {{end}}
        </div>
    </main>
    <!-- Optional JavaScript -->
    <!-- jQuery first, then Popper.js, then Bootstrap JS -->
    <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"
        integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo"
        crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.6/umd/popper.min.js"
        integrity="sha384-wHAiFfRlMFy6i5SRaxvfOCifBUQy1xHdJ/yoi7FRNXMRBu5WHdZYu1hA6ZOblgut"
        crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/js/bootstrap.min.js"
        integrity="sha384-B0UglyR+jN6CkvvICOB2joaf5I4l3gm9GU6Hc1og6Ls7i6U/mkkaduKaBhlAXv9k"
        crossorigin="anonymous"></script>
</body>

</html>
`


```


## まとめ
本記事の内容は以下の通りです。

- 推しのtwitter漫画をより楽しむためのコマンドラインツール「[imgdler](https://github.com/kazdevl/imgdler)」を作りました！
    - “Start”で取得したいツイートを毎21時に取り込む
    - ”List”で読める著者名リストを確認
    - ”Open”でブラウザを開いて、まとめてスムーズに読んでいきます
- 自己責任で使いましょう

このツールの開発期間もまだ数日なので、まだまだ追加すべき機能や課題もありますが、
気に入ってくださった方や興味のある方は是非PullRequestやissueやスターなどをお待ちしております！

https://github.com/kazdevl/imgdler

## 宣伝
開発に協力している[gotwtr](https://github.com/sivchari/gotwtr)ですが、Twitterの公式フォーラムにGoのライブラリとして掲載されていました~!!

https://developer.twitter.com/en/docs/twitter-api/tools-and-libraries/v2