2時間でQiitaとはてブのトレンドをLINEに通知するプログラムを書いてAWSにデプロイした話

# はじめに

出社して、いつものトレンドチェックの日課をしていると、
以下の記事が目に留まりました。

[GitHubActionsでQiitaとSlackを繋いで、エンジニアにとっての良い習慣をつくる。](https://qiita.com/UhRhythm/items/219ee6653f25b63fc6cc)

そういえば自分以外のエンジニアがトレンドチェックしているの見かけたことないぞ🤔

というわけで、毎日チェックしてもらいたい [Qiita トレンド](https://qiita.com/) と [はてなブックマークIT トレンド](https://b.hatena.ne.jp/hotentry/it) を毎朝LINEに通知するシステムを作りました。

LINEアカウントを追加した人に、こんな感じの通知が届きます
![WS000000.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/157186/731b4988-0748-9fd3-d0a6-18863b849be7.jpeg)

# LINE DeveloperでMessaging APIの作成

![WS000000.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/157186/576b1449-5226-c8c2-7bfb-b50bed24388d.jpeg)

まず、息を吸うように新規のMessaging APIを作成します。
名前やアイコンは適当でいいです。

# プログラミング開始

では、組んで行きましょう。

## 環境構築

1. PHPが得意なので、Laravelプロジェクトを作成します。
`$ laravel new line-it-trend-news`

2. LINE BotをPHPで扱うためのSDKをインストールします。
`$ composer require linecorp/line-bot-sdk`

3. スクレイピングしたいので、DomCrawlerをインストールします。
`$ composer require symfony/dom-crawler`

4. AWSのLambdaにデプロイしたいので、[bref](https://bref.sh/) をインストールします。
`$ composer require bref/bref bref/laravel-bridge`
`$ php artisan vendor:publish --tag=serverless-config`

はい、これで環境は整いました。

## プログラミング

### LINE Message APIで実行させるための設定ファイルを作成します

```php5:config/linebot.php
<?php
return [
    'channel_access_token' => getenv('LINE_CHANNEL_ACCESS_TOKEN', null),
    'channel_secret' => getenv('LINE_CHANNEL_SECRET', null),
];
```

```text:.env
LINE_CHANNEL_ACCESS_TOKEN=<Messaging APIのチャネルアクセストークンを記述>
LINE_CHANNEL_SECRET=<Messaging APIのチャネルシークレットを記述>
```

### クーロンで実行したいので、コマンドを作成します。
`$ php artisan make:command SendLineMessageCommand`

中身はこんな感じ
正直、突貫で作ってるのでアンリーダブルです。

```php5:app/Console/Commands/SendLineMessageCommand.php
<?php
namespace App\Console\Commands;

use Illuminate\Console\Command;

use LINE\LINEBot\HTTPClient\CurlHTTPClient;
use LINE\LINEBot;
use LINE\LINEBot\MessageBuilder\TextMessageBuilder;
use Symfony\Component\DomCrawler\Crawler;

class SendLineMessageCommand extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'cron:send_line_message';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Command description';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return int
     */
    public function handle()
    {
        $message = sprintf(
            "【Qiitaのトレンド】\n%s\n\n【はてなのトレンド】\n%s",
            $this->createQiitaTrendMessage(),
            $this->createHatenaTrendMessage()
        );

        $httpClient = new CurlHTTPClient(config('linebot.channel_access_token'));
        $bot = new LINEBot($httpClient, ['channelSecret' => config('linebot.channel_secret')]);
        $textMessageBuilder = new TextMessageBuilder($message);
        $bot->broadcast($textMessageBuilder);

        return 0;
    }

    private function createQiitaTrendMessage() {
        $response = \Http::get('https://qiita.com');

        $crawler = new Crawler($response->getBody()->getContents());
        $node = $crawler->filter('div[data-hyperapp-app="Trend"]')->eq(0);
        $value = $node->attr('data-hyperapp-props');
        
        $message = collect(json_decode($value, true))
            ->dotc('trend.edges', [])
            ->slice(0, 5)
            ->map(function($row) {
                $data = collect($row);
                $subject = $data->dot('node.title');
                $uuid = $data->dot('node.uuid');
                $userName = $data->dot('node.author.userName');
                $url = sprintf('https://qiita.com/%s/items/%s', $userName, $uuid);

                return sprintf('▼ %s%s%s', $subject, "\n", $url);
            })
            ->join("\n\n");

        return $message;
    }

    private function createHatenaTrendMessage() {
        $response = \Http::get('https://b.hatena.ne.jp/hotentry/it');

        $crawler = new Crawler($response->getBody()->getContents());

        $list = collect([]);
        $crawler->filter('.entrylist-contents div.entrylist-contents-main')->each(function($node) use($list) {
            $title = $node->filter('h3 a')->eq(0)->attr('title') ?? null;
            $url   = $node->filter('h3 a')->eq(0)->attr('href') ?? null;
            $list->push([$title, $url]);
        });

        return $list->slice(0, 5)->map(function($data) {
            list($title, $url) = $data;
            return sprintf('▼ %s%s%s', $title, "\n", $url);
        })
        ->join("\n\n");
    }
}
```

### `app/Console/Kernel.php` に追加して指定した時間に実行されるようにします

```php5:app/Console/Kernel.php
    protected function schedule(Schedule $schedule)
    {
        $schedule->command('cron:send_line_message')->dailyAt('10:00');
    }
```

### `serverless.yml` に `app/Console/Kernel.php` を実行するコマンドを追加

`events` を追加することによって、毎分 Cloudwatchが `schedule:run` コマンドを叩いてくれます。

```yml:serverless.yml
    artisan:
        handler: artisan
        timeout: 120
        layers:
            - ${bref:layer.php-74}
            - ${bref:layer.console}
        # ここを追加
        events:
            - schedule:
                rate: rate(1 minute)
                input:
                    cli: schedule:run
```

## デプロイ

ワンコマンドでデプロイできます。
プロファイラや環境などはよしなに

`$ serverless deploy`


## ソース
こちらにそのまま公開しています。
記事書いたときと変わる可能性があります。
https://github.com/YuK1Game/line-it-trend-news


# さいごに

・URLは短縮して短くしたい
・カルーセルテンプレート使って表示したい

など諸々やりたいことはありますが、
即席にしてはなかなかいい感じになったんではないでしょうか。


あとはこれを社内のエンジニアに共有しなくては…