[PYTHON] The Advent calendar has been posted! Notify Slack

It is the 9th day of aratana Advent Calendar 2019. aratana

The company Slack has a channel for advent calendar, and when it is posted on the aratana advent calendar, I will notify Slack. (One hour interval I made it when I was in Advent a year ago, but I'll rewrite it and paste the code. スクリーンショット 2019-12-09 12.59.19.png

Main operation

  1. Notify the "Title, Date, URL" of the posted article
  2. Check for the entire period and notify the posted article (because it may be posted at a later date)
  3. Notify each post

code

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:
            #If not posted
            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

        #Notified is skipped
        if entry.date_str in self.date_cache:
            return False

        return True

Use

Enter the required values as shown below, execute it, and you will be notified of Slack!

url = "https://qiita.com/advent-calendar/2019/aratana"
cache_path = "./list.json"
title = "aratana Advent Calendar Notification of new posts"

slack_notification = SlackNotification(
    [INCOMING_WEBHOOK_Enter the URL!]
)

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

If you change slack_notification to PrintNotification (), you can check the operation without Slack notification!

Recommended Posts

The Advent calendar has been posted! Notify Slack
Notify Slack when the linux command finishes
I checked the calendar deleted in Qiita Advent Calendar 2016
Lazy advent calendar 2019
The site of "OpenCV-Python tutorial document" has been launched.
Looking back on the transition of the Qiita Advent calendar