LoginSignup
7
1

More than 3 years have passed since last update.

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

Posted at

aratana Advent Calendar 2019の9日目です。
aratana

会社Slackにアドベントカレンダー用チャンネルがあり、aratana アドベントカレンダーに投稿されると、Slack通知するようにしてます。(一時間間隔
1年前のアドベントのときに作ったのですが、書き直してコード貼っつけておきます。
スクリーンショット 2019-12-09 12.59.19.png

主な動作

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

コード

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通知されます!

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通知せずに動作確認できます!

7
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
1