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


はじめに

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

GitHubActionsでQiitaとSlackを繋いで、エンジニアにとっての良い習慣をつくる。

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

というわけで、毎日チェックしてもらいたい Qiita トレンドはてなブックマークIT トレンド を毎朝LINEに通知するシステムを作りました。

LINEアカウントを追加した人に、こんな感じの通知が届きます

LINE DeveloperでMessaging APIの作成

まず、息を吸うように新規の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 をインストールします。
    $ composer require bref/bref bref/laravel-bridge
    $ php artisan vendor:publish --tag=serverless-config

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

プログラミング

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

config/linebot.php
<?php
return [
    'channel_access_token' => getenv('LINE_CHANNEL_ACCESS_TOKEN', null),
    'channel_secret' => getenv('LINE_CHANNEL_SECRET', null),
];
.env
LINE_CHANNEL_ACCESS_TOKEN=<Messaging APIのチャネルアクセストークンを記述>
LINE_CHANNEL_SECRET=<Messaging APIのチャネルシークレットを記述>

クーロンで実行したいので、コマンドを作成します。

$ php artisan make:command SendLineMessageCommand

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

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 に追加して指定した時間に実行されるようにします

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

serverless.ymlapp/Console/Kernel.php を実行するコマンドを追加

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

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は短縮して短くしたい
・カルーセルテンプレート使って表示したい

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

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