アドベントカレンダー投稿されたよ！をSlackに通知

[aratana Advent Calendar 2019](https://qiita.com/advent-calendar/2019/aratana)の9日目です。
![aratana](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/150455/e79ba51f-0163-246b-ad7b-998f8f442d72.png)

会社Slackにアドベントカレンダー用チャンネルがあり、aratana アドベントカレンダーに投稿されると、Slack通知するようにしてます。（一時間間隔
1年前のアドベントのときに作ったのですが、書き直してコード貼っつけておきます。
<img width="522" alt="スクリーンショット 2019-12-09 12.59.19.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/218282/96e91330-a9e3-412b-7e09-2e5cac8a2370.png">



# 主な動作
1. 投稿された記事の「タイトル, 日付, URL」を通知
2. 全期間チェックして投稿された記事を通知（後日投稿される場合があるかもなので
3. 一投稿ずつ通知

# コード

```python
from abc import ABCMeta, abstractmethod
from pathlib import Path
from typing import List, Set

import requests
from bs4 import BeautifulSoup


class AdventCalendarEntry:
    def __init__(self, title: str, url: str, date_str: str) -> None:
        self.title = title
        self.url = url
        self.date_str = date_str

    def empty(self) -> bool:
        if self.title:
            return False

        return True


class AdventCalendarEntryList:
    ITEM_SELECTOR = ".adventCalendarItem"
    DATE_SELECTOR = ".adventCalendarItem_date"
    ENTRY_SELECTOR = ".adventCalendarItem_entry"

    def __init__(self, entry_list: List[AdventCalendarEntry]) -> None:
        self._entry_list = entry_list

    def __iter__(self):
        return iter(self._entry_list)

    @classmethod
    def from_html(cls, html_text: str) -> "AdventCalendarEntryList":
        bs = BeautifulSoup(html_text, "lxml")
        item_list_bs = bs.select(cls.ITEM_SELECTOR)

        entry_list = [cls._make_item_from_item_bs(item_bs) for item_bs in item_list_bs]

        return cls(entry_list)

    @staticmethod
    def _make_item_from_item_bs(item_bs):
        entry_date = item_bs.select_one(AdventCalendarEntryList.DATE_SELECTOR).text

        entry_bs = item_bs.select_one(AdventCalendarEntryList.ENTRY_SELECTOR)
        if not entry_bs:
            # 未投稿の場合
            return AdventCalendarEntry(title="", url="", date_str=entry_date)

        _anchor = entry_bs.select_one("a")
        entry_title = _anchor.text
        entry_url = _anchor.get("href")

        return AdventCalendarEntry(
            title=entry_title, url=entry_url, date_str=entry_date
        )


class AdventCalendarDateCache:
    def __init__(self, cache_path: str) -> None:
        self.cache_path = Path(cache_path)
        self.cached_date_set = self.load()

    def load(self) -> Set[str]:
        if not Path(self.cache_path).exists():
            return set()

        with open(self.cache_path, "r") as f:
            checked_list = [line.strip() for line in f.readlines()]

        return set(checked_list)

    def save(self) -> None:
        with open(self.cache_path, "w") as f:
            lines = "\n".join(list(self.cached_date_set))
            f.writelines(lines)

    def add(self, date_str: str) -> None:
        self.cached_date_set.add(date_str)

    def __contains__(self, date_str: str) -> bool:
        return date_str in self.cached_date_set


class Notification(metaclass=ABCMeta):
    @abstractmethod
    def run(self, title: str, entry: AdventCalendarEntry) -> None:
        pass


class PrintNotification(Notification):
    def run(self, title: str, entry: AdventCalendarEntry) -> None:
        message = (
            f"{title}\n"
            f"＜{entry.date_str}＞\n"
            f"TITLE: {entry.title}\n"
            f"URL  : {entry.url}"
        )

        print(message)


class SlackNotification(Notification):
    def __init__(self, webhook_url: str) -> None:
        self.webhook_url = webhook_url

    def run(self, title: str, entry: AdventCalendarEntry) -> None:
        message = (
            f"<!here>\n"
            f"*{title}*\n"
            f"> ＜{entry.date_str}＞\n"
            f"> TITLE: {entry.title}\n"
            f"> URL  : {entry.url}"
        )

        payload = {"text": message, "as_user": True}

        r = requests.post(self.webhook_url, json=payload)
        r.raise_for_status()


class AdventCalendarNotify:
    def __init__(
        self, message_title: str, cache_path: str, notify: Notification
    ) -> None:
        self.message_title = message_title
        self.cache_path = cache_path
        self.notify = notify

        self.date_cache = AdventCalendarDateCache(cache_path)

    def run(self, url: str) -> None:
        for entry in self.load_entry_list(url):
            if not self.new_entry(entry):
                continue

            self.notify.run(self.message_title, entry)
            self.date_cache.add(entry.date_str)

        self.date_cache.save()

    def load_entry_list(self, url: str) -> List[AdventCalendarEntry]:
        r = requests.get(url)
        r.raise_for_status()

        return list(AdventCalendarEntryList.from_html(r.text))

    def new_entry(self, entry: AdventCalendarEntry) -> bool:
        if entry.empty():
            return False

        # 通知済みは、スキップ
        if entry.date_str in self.date_cache:
            return False

        return True
```

# 利用
下記のように必要な値をいれて、実行すれば、Slack通知されます！

```python
url = "https://qiita.com/advent-calendar/2019/aratana"
cache_path = "./list.json"
title = "aratanaアドベントカレンダー新規投稿の通知"

slack_notification = SlackNotification(
    [INCOMING_WEBHOOK_URLを入れる！]
)

notify = AdventCalendarNotify(title, cache_path, slack_notification)
notify.run(url)
```

slack_notificationをところを、`PrintNotification()`に変更すれば、Slack通知せずに動作確認できます！