diff --git a/hackernews/README.md b/hackernews/README.md new file mode 100644 index 00000000..26d54f11 --- /dev/null +++ b/hackernews/README.md @@ -0,0 +1,63 @@ +--- +title: Hacker News Alerts in Slack +description: Track Hacker News articles by topic and send updates to Slack +integrations: ["slack"] +categories: ["Office Automation"] +--- + +# Hacker News Alerts in Slack + +This project monitors Hacker News for new articles matching a specific topic, fetching their details in real-time. It compares the latest results with previously fetched articles to identify new ones and sends their title and URL as notifications to a Slack channel. + +## How It Works + +1. Extract the topic from the Slack app mention. +2. Add the topic to the Hacker News search query (check out [Algolia's REST API](https://www.algolia.com/doc/api-reference/rest-api) for more details). +3. Return all the new articles related to the topic that were published since the last check. + +## Deployment & Configuration + +#### Cloud Usage (Recommended) + +- Initialize your connection with Slack through the UI + +#### Prerequisites + +- [Install AutoKitteh](https://docs.autokitteh.com/get_started/install) +- Set up required integrations: + - [Slack](https://docs.autokitteh.com/integrations/slack) + +#### Installation Steps + +1. Clone the repository: + ```shell + git clone https://github.com/autokitteh/kittehub.git + cd kittehub/hackernews + ``` + +2. Start the AutoKitteh server: + ```shell + ak up --mode dev + ``` + +3. Deploy the project: + ```shell + ak deploy --manifest autokitteh.yaml + ``` + + The output will show your connection IDs, which you'll need for the next step. Look for lines like: + ```shell + [exec] create_connection "hackernews_alert/slack_connection": con_01je39d6frfdtshstfg5qpk8sz created + ``` + + In this example, `con_01je39d6frfdtshstfg5qpk8sz` is the connection ID. + +4. Initialize your connections using the CLI: + ```shell + ak connection init slack_connection + ``` + +## Trigger Workflow + +- Type `@your-slack-app topic` in the Slack channel you set in the environment variable, replacing `topic` with what you want to search for, to start tracking articles +- The workflow runs automatically every two minutes after deployment diff --git a/hackernews/autokitteh.yaml b/hackernews/autokitteh.yaml new file mode 100644 index 00000000..ea7f8ba6 --- /dev/null +++ b/hackernews/autokitteh.yaml @@ -0,0 +1,18 @@ +# This YAML file is a declarative manifest that describes the setup of an +# AutoKitteh project that monitors Hacker News for a specific topic. + +version: v1 + +project: + name: hackernews_alert + vars: + - name: POLLING_INTERVAL_SECS + value: 120 + connections: + - name: slack_connection + integration: slack + triggers: + - name: slack_slash_command + connection: slack_connection + event_type: app_mention + call: program.py:on_slack_command diff --git a/hackernews/program.py b/hackernews/program.py new file mode 100644 index 00000000..f5512c61 --- /dev/null +++ b/hackernews/program.py @@ -0,0 +1,56 @@ +"""Monitor Hacker News for new articles on a specific topic, and post updates to a Slack channel.""" + +import os +import requests +import time +import urllib.parse + +from autokitteh.slack import slack_client + + +API_URL = "http://hn.algolia.com/api/v1/search_by_date?tags=story&page=0&query=" +POLLING_INTERVAL_SECS = int(os.getenv("POLLING_INTERVAL_SECS")) + +slack = slack_client("slack_connection") + + +def on_slack_command(event): + """Workflow's entry-point. + Extracts a topic from a Slack command, monitors for new articles, + and posts updates to `SLACK_CHANNEL`. + """ + topic = event.data.text.split(" ", 1)[-1].strip() + slack.chat_postMessage( + channel=event.data.channel, + text=f"Waiting for new articles on the topic: `{topic}`.", + ) + current_articles = set() + fetch_articles(topic, current_articles) + + # NOTE: For low-traffic topics, it might take a while for new articles to be published, + # so users may experience delays in receiving notifications. + while True: + all_articles = set(current_articles) + fetch_articles(topic, all_articles) + new_articles = all_articles - current_articles + + for article in new_articles: + _, title, url = article + slack_message = f"Title: {title}, URL: {url if url else 'No URL'}" + slack.chat_postMessage(channel=event.data.channel, text=slack_message) + current_articles.update(new_articles) + + time.sleep(POLLING_INTERVAL_SECS) + + +def fetch_articles(topic, all_articles): + encoded_query = urllib.parse.quote(topic) + full_url = f"{API_URL}{encoded_query}" + hits = requests.get(full_url).json().get("hits", []) + + # Extract some of the article fields from the API response. + for article in hits: + object_id = article["objectID"] + title = article["title"] + url = article.get("url") + all_articles.add((object_id, title, url))