diff --git a/.github/workflows/feedbot.yml b/.github/workflows/feedbot.yml
index 2afff40..9ffd94f 100644
--- a/.github/workflows/feedbot.yml
+++ b/.github/workflows/feedbot.yml
@@ -17,8 +17,8 @@ jobs:
uses: actions/cache@v2
with:
path: ./slackfeedbot-cache
- key: feed-cache-e-${{ steps.generate-key.outputs.cache-key }}
- restore-keys: feed-cache-e-
+ key: feed-cache-i-${{ steps.generate-key.outputs.cache-key }}
+ restore-keys: feed-cache-i-
- name: LAT
uses: 'selfagency/slackfeedbot@dev'
with:
diff --git a/README.md b/README.md
index 38888da..0c65edf 100644
--- a/README.md
+++ b/README.md
@@ -14,17 +14,21 @@ Push RSS feed updates to Slack via GitHub Actions
4. Create a new workflow in your desired repository (e.g. `.github/workflows/slackfeedbot.yml`) and drop in the follwing, where:
- - `rss` is an RSS feed URL.
- - `slack_webhook` is the URL of your Slack webhook (this can and probably
+ - `rss`: An RSS feed URL.
+ - `slack_webhook`: The URL of your Slack webhook (this can and probably
should be a repository or organization secret).
- - `cache_dir` is the folder in which you want to cache RSS data to prevent
+ - `cache_dir`: The folder in which you want to cache RSS data to prevent
publishing duplicates (e.g., `./slackfeedbot-cache`), or alternately...
- - `interval` is the number of minutes between runs of the parent workflow, as
+ - `interval`: The number of minutes between runs of the parent workflow, as
specified in the `cron` section of the `schedule` workflow trigger (may
publish duplicates due to post pinning).
- - `unfurl` tells Slack to show the [Open Graph](https://ogp.me/) preview. If
- set to `false` the title, date, short description, and a link to the feed item
- will be posted. Defaults to `false` because it's kind of flaky.
+ - `unfurl`: Tells Slack to show the [Open Graph](https://ogp.me/) preview.
+ Defaults to `false` because it's kind of flaky. Not customizable. Use
+ the below settings for customizd display.
+ - `show_desc`: Whether to show the post description. Defaults to `true`.
+ - `show_img`: Whether to show the post image. Defaults to `true`.
+ - `show_date`: Whether to show the post date. Defaults to `true`.
+ - `show_link`: Whether to show the Read more link, linking back to the post. Defaults to `true`.
## Examples
@@ -54,7 +58,7 @@ jobs:
key: feed-cache-${{ steps.generate-key.outputs.cache-key }}
restore-keys: feed-cache-
- name: NYT
- uses: 'selfagency/slackfeedbot@v1.2.6'
+ uses: 'selfagency/slackfeedbot@v1.2.7'
with:
rss: 'https://rss.nytimes.com/services/xml/rss/nyt/HomePage.xml'
slack_webhook: ${{ secrets.SLACK_WEBHOOK }}
@@ -75,45 +79,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: NYT
- uses: 'selfagency/slackfeedbot@v1.2.6'
+ uses: 'selfagency/slackfeedbot@v1.2.7'
with:
rss: 'https://rss.nytimes.com/services/xml/rss/nyt/HomePage.xml'
slack_webhook: ${{ secrets.SLACK_WEBHOOK }}
interval: 15
```
-### Unfurl URLs
-
-```
-name: FeedBot
-on:
- schedule:
- - cron: '*/15 * * * *'
-jobs:
- rss-to-slack:
- runs-on: ubuntu-latest
- steps:
- - name: Generate cache key
- uses: actions/github-script@v6
- id: generate-key
- with:
- script: |
- core.setOutput('cache-key', new Date().valueOf())
- - name: Retrieve cache
- uses: actions/cache@v2
- with:
- path: ./slackfeedbot-cache
- key: feed-cache-${{ steps.generate-key.outputs.cache-key }}
- restore-keys: feed-cache-
- - name: NYT
- uses: 'selfagency/slackfeedbot@v1.2.6'
- with:
- rss: 'https://rss.nytimes.com/services/xml/rss/nyt/HomePage.xml'
- slack_webhook: ${{ secrets.SLACK_WEBHOOK }}
- cache_dir: './slackfeedbot-cache'
- unfurl: true
-```
-
### Multiple feeds
```
@@ -138,13 +110,13 @@ jobs:
key: feed-cache-${{ steps.generate-key.outputs.cache-key }}
restore-keys: feed-cache-
- name: LAT
- uses: 'selfagency/slackfeedbot@v1.2.6'
+ uses: 'selfagency/slackfeedbot@v1.2.7'
with:
rss: 'https://www.latimes.com/rss2.0.xml'
slack_webhook: ${{ secrets.SLACK_WEBHOOK }}
cache_dir: './slackfeedbot-cache'
- name: WaPo
- uses: 'selfagency/slackfeedbot@v1.2.6'
+ uses: 'selfagency/slackfeedbot@v1.2.7'
with:
rss: 'https://feeds.washingtonpost.com/rss/homepage'
slack_webhook: ${{ secrets.SLACK_WEBHOOK }}
diff --git a/action.yml b/action.yml
index 6a0d79f..c8bebf0 100644
--- a/action.yml
+++ b/action.yml
@@ -17,6 +17,15 @@ inputs:
interval:
description: 'Minutes between workflow runs'
required: false
+ show_date:
+ description: 'Show date in output'
+ required: false
+ show_link:
+ description: 'Show link in output'
+ required: false
+ show_description:
+ description: 'Show description in output'
+ required: false
unfurl:
description: 'Unfurl links'
required: false
diff --git a/dist/index.cjs b/dist/index.cjs
index e819813..7d83873 100644
--- a/dist/index.cjs
+++ b/dist/index.cjs
@@ -20070,6 +20070,178 @@ var require_showdown = __commonJS({
}
});
+// node_modules/striptags/src/striptags.js
+var require_striptags = __commonJS({
+ "node_modules/striptags/src/striptags.js"(exports, module2) {
+ "use strict";
+ (function(global2) {
+ if (typeof Symbol2 !== "function") {
+ var Symbol2 = function(name) {
+ return name;
+ };
+ Symbol2.nonNative = true;
+ }
+ const STATE_PLAINTEXT = Symbol2("plaintext");
+ const STATE_HTML = Symbol2("html");
+ const STATE_COMMENT = Symbol2("comment");
+ const ALLOWED_TAGS_REGEX = /<(\w*)>/g;
+ const NORMALIZE_TAG_REGEX = /<\/?([^\s\/>]+)/;
+ function striptags2(html, allowable_tags, tag_replacement) {
+ html = html || "";
+ allowable_tags = allowable_tags || [];
+ tag_replacement = tag_replacement || "";
+ let context = init_context(allowable_tags, tag_replacement);
+ return striptags_internal(html, context);
+ }
+ function init_striptags_stream(allowable_tags, tag_replacement) {
+ allowable_tags = allowable_tags || [];
+ tag_replacement = tag_replacement || "";
+ let context = init_context(allowable_tags, tag_replacement);
+ return function striptags_stream(html) {
+ return striptags_internal(html || "", context);
+ };
+ }
+ striptags2.init_streaming_mode = init_striptags_stream;
+ function init_context(allowable_tags, tag_replacement) {
+ allowable_tags = parse_allowable_tags(allowable_tags);
+ return {
+ allowable_tags,
+ tag_replacement,
+ state: STATE_PLAINTEXT,
+ tag_buffer: "",
+ depth: 0,
+ in_quote_char: ""
+ };
+ }
+ function striptags_internal(html, context) {
+ if (typeof html != "string") {
+ throw new TypeError("'html' parameter must be a string");
+ }
+ let allowable_tags = context.allowable_tags;
+ let tag_replacement = context.tag_replacement;
+ let state = context.state;
+ let tag_buffer = context.tag_buffer;
+ let depth = context.depth;
+ let in_quote_char = context.in_quote_char;
+ let output = "";
+ for (let idx = 0, length = html.length; idx < length; idx++) {
+ let char = html[idx];
+ if (state === STATE_PLAINTEXT) {
+ switch (char) {
+ case "<":
+ state = STATE_HTML;
+ tag_buffer += char;
+ break;
+ default:
+ output += char;
+ break;
+ }
+ } else if (state === STATE_HTML) {
+ switch (char) {
+ case "<":
+ if (in_quote_char) {
+ break;
+ }
+ depth++;
+ break;
+ case ">":
+ if (in_quote_char) {
+ break;
+ }
+ if (depth) {
+ depth--;
+ break;
+ }
+ in_quote_char = "";
+ state = STATE_PLAINTEXT;
+ tag_buffer += ">";
+ if (allowable_tags.has(normalize_tag(tag_buffer))) {
+ output += tag_buffer;
+ } else {
+ output += tag_replacement;
+ }
+ tag_buffer = "";
+ break;
+ case '"':
+ case "'":
+ if (char === in_quote_char) {
+ in_quote_char = "";
+ } else {
+ in_quote_char = in_quote_char || char;
+ }
+ tag_buffer += char;
+ break;
+ case "-":
+ if (tag_buffer === "":
+ if (tag_buffer.slice(-2) == "--") {
+ state = STATE_PLAINTEXT;
+ }
+ tag_buffer = "";
+ break;
+ default:
+ tag_buffer += char;
+ break;
+ }
+ }
+ }
+ context.state = state;
+ context.tag_buffer = tag_buffer;
+ context.depth = depth;
+ context.in_quote_char = in_quote_char;
+ return output;
+ }
+ function parse_allowable_tags(allowable_tags) {
+ let tag_set = /* @__PURE__ */ new Set();
+ if (typeof allowable_tags === "string") {
+ let match;
+ while (match = ALLOWED_TAGS_REGEX.exec(allowable_tags)) {
+ tag_set.add(match[1]);
+ }
+ } else if (!Symbol2.nonNative && typeof allowable_tags[Symbol2.iterator] === "function") {
+ tag_set = new Set(allowable_tags);
+ } else if (typeof allowable_tags.forEach === "function") {
+ allowable_tags.forEach(tag_set.add, tag_set);
+ }
+ return tag_set;
+ }
+ function normalize_tag(tag_buffer) {
+ let match = NORMALIZE_TAG_REGEX.exec(tag_buffer);
+ return match ? match[1].toLowerCase() : null;
+ }
+ if (typeof define === "function" && define.amd) {
+ define(function module_factory() {
+ return striptags2;
+ });
+ } else if (typeof module2 === "object" && module2.exports) {
+ module2.exports = striptags2;
+ } else {
+ global2.striptags = striptags2;
+ }
+ })(exports);
+ }
+});
+
// node_modules/web-streams-polyfill/dist/ponyfill.es2018.js
var require_ponyfill_es2018 = __commonJS({
"node_modules/web-streams-polyfill/dist/ponyfill.es2018.js"(exports, module2) {
@@ -24479,7 +24651,7 @@ var checkCache = async (rss, cached) => {
if (!cacheHit)
output.push(item);
}
- import_core.default.debug(`Found ${output.length} new items`);
+ import_core.default.debug(`Found ${output.length} uncached items`);
return output;
} else {
import_core.default.debug("Nothing to check");
@@ -24524,11 +24696,13 @@ var getFeed = async (rssFeed, cacheDir, interval) => {
import_core2.default.debug(`Retrieving previously cached entries\u2026`);
try {
cached = await readCache(rssFeed, cacheDir);
- toSend = await checkCache(rss, cached);
+ toSend = (await checkCache(rss, cached)).filter((item) => {
+ return (0, import_dayjs.default)(item.created).isAfter((0, import_dayjs.default)().subtract(1, "hour"));
+ });
} catch (err) {
import_core2.default.debug(err.message);
toSend = rss.items.filter((item) => {
- return (0, import_dayjs.default)(item.created).isAfter((0, import_dayjs.default)().subtract(60, "minute"));
+ return (0, import_dayjs.default)(item.created).isAfter((0, import_dayjs.default)().subtract(1, "hour"));
});
}
} else if (interval) {
@@ -24545,6 +24719,7 @@ var getFeed = async (rssFeed, cacheDir, interval) => {
// src/lib/payload.ts
var import_core4 = __toESM(require_core(), 1);
+var import_dayjs2 = __toESM(require_dayjs_min(), 1);
var import_html_to_text = __toESM(require_html_to_text2(), 1);
// node_modules/linkedom/esm/shared/symbols.js
@@ -28755,6 +28930,7 @@ setPrototypeOf(Document3, Document).prototype = Document.prototype;
// src/lib/payload.ts
var import_showdown = __toESM(require_showdown(), 1);
+var import_striptags = __toESM(require_striptags(), 1);
// src/lib/feedimg.ts
var import_core3 = __toESM(require_core(), 1);
@@ -29945,39 +30121,63 @@ var getFeedImg = async (rssFeed) => {
// src/lib/payload.ts
var converter = new import_showdown.default.Converter();
var html2txt = (0, import_html_to_text.compile)({
- wordwrap: 120
+ wordwrap: 255
});
-var genPayload = async (filtered, unfiltered, rssFeed, unfurl) => {
+var genPayload = async (filtered, unfiltered, rssFeed, unfurl, showDesc, showImg, showDate, showLink) => {
try {
- const blocks = filtered.map((item) => {
+ const blocks = [];
+ filtered.forEach((item) => {
+ var _a;
let text = "";
if (!unfurl) {
- if (item.title)
- text += `*${html2txt(item.title)}*
-`;
if (item.description) {
const { document: document2 } = parseHTML("
");
- let desc = item.description;
- if (/>.+</.test(item.description)) {
- desc = item.description.replace(/>/g, ">").replace(/</g, "<").replace(/\n/g, "").replace(/
/g, "\n").replace(/\\\\-/g, "-");
- }
+ const desc = (0, import_striptags.default)(item.description.replace(/>/g, ">").replace(/</g, "<"), ["p", "strong", "b", "em", "i", "a", "ul", "ol", "li"], " ");
const markdown = converter.makeMarkdown(desc, document2);
- text += `${html2txt(markdown).replace(/[Rr]ead more/g, "\u2026")}
-`;
+ text += `${markdown.replace(/\\-/g, "-").replace(/\\\|/g, "|")}`;
}
- if (item.link)
- text += `<${item.link}|Read more>`;
- } else {
- if (item.title)
- text += `<${item.link}|${html2txt(item.title + item.created)}>`;
}
- return {
- type: "section",
- text: {
- type: "mrkdwn",
- text
+ if (item == null ? void 0 : item.title) {
+ blocks.push({
+ type: "header",
+ text: { type: "plain_text", text: html2txt(item == null ? void 0 : item.title) }
+ });
+ }
+ if (unfurl) {
+ blocks.push({
+ type: "section",
+ text: {
+ type: "mrkdwn",
+ text: `<${item == null ? void 0 : item.link}|Read more>`
+ }
+ });
+ } else {
+ const fields = [];
+ if (showLink) {
+ fields.push({
+ type: "mrkdwn",
+ text: `<${item == null ? void 0 : item.link}|Read more>`
+ });
}
- };
+ blocks.push({
+ type: "section",
+ fields,
+ accessory: showImg && item.image ? {
+ type: "image",
+ image_url: item.image
+ } : void 0,
+ text: showDesc && !text.trim().toLowerCase().startsWith("read more") ? {
+ type: "mrkdwn",
+ text
+ } : void 0
+ });
+ if (showDate) {
+ blocks.push({
+ type: "context",
+ elements: [{ type: "mrkdwn", text: `Published ${(_a = (0, import_dayjs2.default)(item == null ? void 0 : item.created)) == null ? void 0 : _a.format("MMM D @ h:mma")} UTC` }]
+ });
+ }
+ }
});
const payload = {
as_user: false,
@@ -30013,18 +30213,32 @@ var slack = async (payload, webhook) => {
var import_core6 = __toESM(require_core(), 1);
var validate = () => {
import_core6.default.debug(`Validating inputs\u2026`);
- if (!import_core6.default.getInput("rss") || !import_core6.default.getInput("rss").startsWith("http")) {
+ import_core6.default.debug(`Inputs: ${JSON.stringify({
+ slackWebhook: import_core6.default.getInput("slack_webhook"),
+ rssFeed: import_core6.default.getInput("rss"),
+ cacheDir: import_core6.default.getInput("cache_dir"),
+ interval: import_core6.default.getInput("interval"),
+ unfurl: import_core6.default.getInput("unfurl"),
+ showDesc: import_core6.default.getInput("show_desc"),
+ showLink: import_core6.default.getInput("show_link"),
+ showDate: import_core6.default.getInput("show_date"),
+ showImg: import_core6.default.getInput("show_img")
+ })}`);
+ if (import_core6.default.getInput("rss").length === 0 || !import_core6.default.getInput("rss").startsWith("http")) {
throw new Error("No feed or invalid feed specified");
}
- if (!import_core6.default.getInput("slack_webhook") || !import_core6.default.getInput("slack_webhook").startsWith("https")) {
+ if (import_core6.default.getInput("slack_webhook").length === 0 || !import_core6.default.getInput("slack_webhook").startsWith("https")) {
throw new Error("No Slack webhook or invalid webhook specified");
}
- if (!import_core6.default.getInput("interval") && !import_core6.default.getInput("cache_dir")) {
+ if (import_core6.default.getInput("interval").length === 0 && import_core6.default.getInput("cache_dir").length === 0) {
throw new Error("No interval or cache folder specified");
}
- if (import_core6.default.getInput("interval") && parseInt(import_core6.default.getInput("interval")).toString() === "NaN") {
+ if (import_core6.default.getInput("interval").length > 0 && parseInt(import_core6.default.getInput("interval")).toString() === "NaN") {
throw new Error("Invalid interval specified");
}
+ if (import_core6.default.getInput("unfurl").length > 0 && import_core6.default.getBooleanInput("unfurl") && (import_core6.default.getInput("show_desc").length > 0 || import_core6.default.getInput("show_link").length > 0 || import_core6.default.getInput("show_date").length > 0 || import_core6.default.getInput("show_img").length > 0)) {
+ throw new Error("Unfurled links cannot be styled with `show` options");
+ }
};
// src/action.ts
@@ -30034,16 +30248,25 @@ var run = async () => {
const slackWebhook = import_core7.default.getInput("slack_webhook");
const rssFeed = import_core7.default.getInput("rss");
const cacheDir = import_core7.default.getInput("cache_dir");
- const interval = import_core7.default.getInput("interval") ? parseInt(import_core7.default.getInput("interval")) : void 0;
- let unfurl = false;
- try {
- unfurl = import_core7.default.getBooleanInput("unfurl");
- } catch (err) {
- import_core7.default.debug(err.message);
- }
+ const interval = import_core7.default.getInput("interval").length > 0 ? parseInt(import_core7.default.getInput("interval")) : void 0;
+ const unfurl = import_core7.default.getInput("unfurl").length > 0 ? import_core7.default.getBooleanInput("unfurl") : false;
+ const showDesc = import_core7.default.getInput("show_desc").length > 0 ? import_core7.default.getBooleanInput("show_desc") : true;
+ const showLink = import_core7.default.getInput("show_link").length > 0 ? import_core7.default.getBooleanInput("show_link") : true;
+ const showDate = import_core7.default.getInput("show_date").length > 0 ? import_core7.default.getBooleanInput("show_date") : true;
+ const showImg = import_core7.default.getInput("show_img").length > 0 ? import_core7.default.getBooleanInput("show_img") : true;
+ import_core7.default.debug(`Processed inputs: ${JSON.stringify({
+ slackWebhook,
+ rssFeed,
+ cacheDir,
+ interval,
+ unfurl,
+ showDesc,
+ showLink,
+ showDate
+ })}`);
const { filtered, unfiltered, cached } = await getFeed(rssFeed, cacheDir, interval);
if (filtered.length) {
- const payload = await genPayload(filtered, unfiltered, rssFeed, unfurl);
+ const payload = await genPayload(filtered, unfiltered, rssFeed, unfurl, showDesc, showImg, showDate, showLink);
await slack(payload, slackWebhook);
if (cacheDir)
await writeCache((unfiltered == null ? void 0 : unfiltered.title) || "", rssFeed, cacheDir, filtered, cached);
diff --git a/package-lock.json b/package-lock.json
index 2f9cb18..6835eb0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "slackfeedbot",
- "version": "1.2.6",
+ "version": "1.2.7",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "slackfeedbot",
- "version": "1.2.6",
+ "version": "1.2.7",
"license": "MIT",
"dependencies": {
"@actions/core": "^1.6.0",
@@ -16,7 +16,8 @@
"node-fetch": "^3.2.3",
"object-sha": "^2.0.6",
"rss-to-json": "^2.0.2",
- "showdown": "^2.0.3"
+ "showdown": "^2.0.3",
+ "striptags": "^3.2.0"
},
"devDependencies": {
"@types/html-to-text": "^8.1.0",
@@ -4228,6 +4229,11 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/striptags": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/striptags/-/striptags-3.2.0.tgz",
+ "integrity": "sha512-g45ZOGzHDMe2bdYMdIvdAfCQkCTDMGBazSw1ypMowwGIee7ZQ5dU0rBJ8Jqgl+jAKIv4dbeE1jscZq9wid1Tkw=="
+ },
"node_modules/strnum": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz",
@@ -7385,6 +7391,11 @@
"integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
"dev": true
},
+ "striptags": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/striptags/-/striptags-3.2.0.tgz",
+ "integrity": "sha512-g45ZOGzHDMe2bdYMdIvdAfCQkCTDMGBazSw1ypMowwGIee7ZQ5dU0rBJ8Jqgl+jAKIv4dbeE1jscZq9wid1Tkw=="
+ },
"strnum": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz",
diff --git a/package.json b/package.json
index 503f159..0e6497b 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "slackfeedbot",
- "version": "1.2.6",
+ "version": "1.2.7",
"description": "Push RSS feed updates to Slack via GitHub Actions",
"repository": {
"type": "git",
@@ -32,7 +32,8 @@
"node-fetch": "^3.2.3",
"object-sha": "^2.0.6",
"rss-to-json": "^2.0.2",
- "showdown": "^2.0.3"
+ "showdown": "^2.0.3",
+ "striptags": "^3.2.0"
},
"devDependencies": {
"@types/html-to-text": "^8.1.0",
diff --git a/src/action.ts b/src/action.ts
index 3adc897..5af1513 100644
--- a/src/action.ts
+++ b/src/action.ts
@@ -7,32 +7,44 @@ import { validate } from './lib/validate';
const run = async () => {
try {
- // validate inputs
+ // Validate inputs
validate();
- // parse inputs
+ // Parse inputs
const slackWebhook = core.getInput('slack_webhook');
const rssFeed = core.getInput('rss');
const cacheDir = core.getInput('cache_dir');
- const interval = core.getInput('interval') ? parseInt(core.getInput('interval')) : undefined;
- let unfurl = false;
- try {
- unfurl = core.getBooleanInput('unfurl');
- } catch (err) {
- core.debug((err).message);
- }
+ const interval = core.getInput('interval').length > 0 ? parseInt(core.getInput('interval')) : undefined;
+ const unfurl = core.getInput('unfurl').length > 0 ? core.getBooleanInput('unfurl') : false;
+ const showDesc = core.getInput('show_desc').length > 0 ? core.getBooleanInput('show_desc') : true;
+ const showLink = core.getInput('show_link').length > 0 ? core.getBooleanInput('show_link') : true;
+ const showDate = core.getInput('show_date').length > 0 ? core.getBooleanInput('show_date') : true;
+ const showImg = core.getInput('show_img').length > 0 ? core.getBooleanInput('show_img') : true;
+
+ core.debug(
+ `Processed inputs: ${JSON.stringify({
+ slackWebhook,
+ rssFeed,
+ cacheDir,
+ interval,
+ unfurl,
+ showDesc,
+ showLink,
+ showDate
+ })}`
+ );
- // get rss feed items
+ // Get RSS feed items
const { filtered, unfiltered, cached } = await getFeed(rssFeed, cacheDir, interval);
if (filtered.length) {
- // generate payload
- const payload = await genPayload(filtered, unfiltered, rssFeed, unfurl);
+ // Generate payload
+ const payload = await genPayload(filtered, unfiltered, rssFeed, unfurl, showDesc, showImg, showDate, showLink);
- // send payload to slack
+ // Send payload to Slack
await slack(payload, slackWebhook);
- // cache data
+ // Save cache data
if (cacheDir) await writeCache(unfiltered?.title || '', rssFeed, cacheDir, filtered, cached);
} else {
core.info(`No new items found`);
diff --git a/src/lib/cache.ts b/src/lib/cache.ts
index 61a6547..4d9cf25 100644
--- a/src/lib/cache.ts
+++ b/src/lib/cache.ts
@@ -8,11 +8,12 @@ const read = promisify(fs.readFile);
const write = promisify(fs.writeFile);
const md = promisify(fs.mkdir);
+// Defines a CacheRecord, which is used to check if an item has already been published
class CacheRecord {
[index: string]: string | number | undefined;
- feedTitle?: string;
- title?: string;
- date?: string | number;
+ feedTitle?: string; // title of the rss feed
+ title?: string; // title of the post
+ date?: string | number; // publish date of the post
constructor(feedTitle?: string, title?: string, date?: number | string) {
this.feedTitle = feedTitle;
@@ -21,15 +22,19 @@ class CacheRecord {
}
}
+// For hashing the CacheRecord
const hash = (str: string): string => {
return createHash('sha256').update(str).digest('hex');
};
+// Generates a hashable string from the CacheRecord
const cacheSlug = (item: CacheRecord): string => {
const { feedTitle, title, created } = item;
+
let slug = '';
slug += feedTitle?.toLowerCase().replace(/[^a-z0-9]/g, '-');
slug += title?.toLowerCase().replace(/[^a-z0-9]/g, '-');
+
if (created) {
const date = new Date(created);
slug += `${slug}-${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`;
@@ -37,6 +42,7 @@ const cacheSlug = (item: CacheRecord): string => {
return slug;
};
+// Reads the cache file and returns it as an array of hashes
const readCache = async (rssFeed: string, cacheDir: string): Promise => {
try {
core.debug(`Retrieving previously published entries…`);
@@ -53,15 +59,19 @@ const readCache = async (rssFeed: string, cacheDir: string): Promise =
}
};
+// Checks if an item has already been published by comparing hashed CacheRecords
const checkCache = async (rss: RssFeed, cached: string[]): Promise => {
try {
if (rss?.items) {
const output = [];
+ // For each item in the RSS feed
for (const item of rss.items) {
let cacheHit = false;
+ // Compare the item to the cached items
for (const published in cached) {
const record = new CacheRecord(rss.title, item.title, item.created);
+
if (cached[published] === hash(cacheSlug(record))) {
cacheHit = true;
core.debug(`Cache hit for ${item.title}`);
@@ -71,7 +81,7 @@ const checkCache = async (rss: RssFeed, cached: string[]): Promise => {
const url = new URL(rssFeed);
const host = url.hostname
diff --git a/src/lib/getfeed.ts b/src/lib/getfeed.ts
index df90695..d7f2dc0 100644
--- a/src/lib/getfeed.ts
+++ b/src/lib/getfeed.ts
@@ -4,31 +4,39 @@ import { parse } from 'rss-to-json';
import { RssFeed, RssFeedItem } from '../types.d';
import { checkCache, readCache } from './cache';
+// Gets the feed, checks the cache (or time since last run), and returns new items
const getFeed = async (
rssFeed: string,
cacheDir: string | undefined,
interval: number | undefined
): Promise<{ filtered: RssFeedItem[]; unfiltered: RssFeed; cached: string[] }> => {
core.debug(`Retrieving ${rssFeed}…`);
+
const rss: RssFeed = await parse(rssFeed, {});
core.debug(`Feed has ${rss?.items?.length} items`);
if (rss?.items?.length) {
let toSend: RssFeedItem[] = [];
let cached: string[] = [];
+
if (cacheDir) {
core.debug(`Retrieving previously cached entries…`);
+
try {
cached = await readCache(rssFeed, cacheDir);
- toSend = await checkCache(rss, cached);
+ toSend = (await checkCache(rss, cached)).filter(item => {
+ return dayjs(item.created).isAfter(dayjs().subtract(1, 'hour'));
+ });
} catch (err) {
core.debug((err).message);
+
toSend = rss.items.filter(item => {
- return dayjs(item.created).isAfter(dayjs().subtract(60, 'minute'));
+ return dayjs(item.created).isAfter(dayjs().subtract(1, 'hour'));
});
}
} else if (interval) {
core.debug(`Selecting items posted in the last ${interval} minutes…`);
+
toSend = rss.items.filter(item => {
return dayjs(item.created).isAfter(dayjs().subtract(interval, 'minute'));
});
diff --git a/src/lib/payload.ts b/src/lib/payload.ts
index 10de442..a921145 100644
--- a/src/lib/payload.ts
+++ b/src/lib/payload.ts
@@ -1,54 +1,98 @@
import core from '@actions/core';
+import dayjs from 'dayjs';
import { compile } from 'html-to-text';
import { parseHTML } from 'linkedom';
import showdown from 'showdown';
+import striptags from 'striptags';
import type { Block, Payload, RssFeed, RssFeedItem } from '../types.d';
import { getFeedImg } from './feedimg';
const converter = new showdown.Converter();
const html2txt = compile({
- wordwrap: 120
+ wordwrap: 255
});
+// Generates the payload to publish to Slack
const genPayload = async (
filtered: RssFeedItem[],
unfiltered: RssFeed,
rssFeed: string,
- unfurl: boolean
+ unfurl: boolean,
+ showDesc: boolean,
+ showImg: boolean,
+ showDate: boolean,
+ showLink: boolean
): Promise => {
try {
- const blocks: Block[] = filtered.map(item => {
+ const blocks: Block[] = [];
+ filtered.forEach(item => {
let text = '';
if (!unfurl) {
- if (item.title) text += `*${html2txt(item.title)}*\n`;
if (item.description) {
// core.debug(`Item description: ${item.description}`);
const { document } = parseHTML('');
- let desc = item.description;
- if (/>.+</.test(item.description)) {
- desc = item.description
- .replace(/>/g, '>')
- .replace(/</g, '<')
- .replace(/\n/g, '')
- .replace(/
/g, '\n')
- .replace(/\\\\-/g, '-');
- }
+ const desc = striptags(
+ item.description.replace(/>/g, '>').replace(/</g, '<'),
+ ['p', 'strong', 'b', 'em', 'i', 'a', 'ul', 'ol', 'li'],
+ ' '
+ );
const markdown = converter.makeMarkdown(desc, document);
- text += `${html2txt(markdown).replace(/[Rr]ead more/g, '…')}\n`;
+ text += `${markdown.replace(/\\-/g, '-').replace(/\\\|/g, '|')}`;
}
- if (item.link) text += `<${item.link}|Read more>`;
- } else {
- if (item.title) text += `<${item.link}|${html2txt(item.title + item.created)}>`;
}
- return {
- type: 'section',
- text: {
- type: 'mrkdwn',
- text
+ if (item?.title) {
+ blocks.push({
+ type: 'header',
+ text: { type: 'plain_text', text: html2txt(item?.title) }
+ });
+ }
+
+ if (unfurl) {
+ blocks.push({
+ type: 'section',
+ text: {
+ type: 'mrkdwn',
+ text: `<${item?.link}|Read more>`
+ }
+ });
+ } else {
+ const fields = [];
+
+ if (showLink) {
+ fields.push({
+ type: 'mrkdwn',
+ text: `<${item?.link}|Read more>`
+ });
}
- };
+
+ blocks.push({
+ type: 'section',
+ fields,
+ accessory:
+ showImg && item.image
+ ? {
+ type: 'image',
+ image_url: item.image
+ }
+ : undefined,
+ text:
+ showDesc && !text.trim().toLowerCase().startsWith('read more')
+ ? {
+ type: 'mrkdwn',
+ text
+ }
+ : undefined
+ });
+
+ if (showDate) {
+ blocks.push({
+ type: 'context',
+ elements: [{ type: 'mrkdwn', text: `Published ${dayjs(item?.created)?.format('MMM D @ h:mma')} UTC` }]
+ });
+ }
+ }
});
const payload = {
diff --git a/src/lib/slack.ts b/src/lib/slack.ts
index 7fc0446..e553a26 100644
--- a/src/lib/slack.ts
+++ b/src/lib/slack.ts
@@ -2,6 +2,7 @@ import core from '@actions/core';
import fetch from 'node-fetch';
import { Payload } from '../types';
+// Publishes messages to Slack
const slack = async (payload: Payload, webhook: string) => {
const res = await fetch(webhook, {
method: 'POST',
diff --git a/src/lib/validate.ts b/src/lib/validate.ts
index d5043c8..7b10889 100644
--- a/src/lib/validate.ts
+++ b/src/lib/validate.ts
@@ -1,23 +1,49 @@
import core from '@actions/core';
+// Validate inputs
const validate = (): void => {
core.debug(`Validating inputs…`);
- if (!core.getInput('rss') || !core.getInput('rss').startsWith('http')) {
+ core.debug(
+ `Inputs: ${JSON.stringify({
+ slackWebhook: core.getInput('slack_webhook'),
+ rssFeed: core.getInput('rss'),
+ cacheDir: core.getInput('cache_dir'),
+ interval: core.getInput('interval'),
+ unfurl: core.getInput('unfurl'),
+ showDesc: core.getInput('show_desc'),
+ showLink: core.getInput('show_link'),
+ showDate: core.getInput('show_date'),
+ showImg: core.getInput('show_img')
+ })}`
+ );
+
+ if (core.getInput('rss').length === 0 || !core.getInput('rss').startsWith('http')) {
throw new Error('No feed or invalid feed specified');
}
- if (!core.getInput('slack_webhook') || !core.getInput('slack_webhook').startsWith('https')) {
+ if (core.getInput('slack_webhook').length === 0 || !core.getInput('slack_webhook').startsWith('https')) {
throw new Error('No Slack webhook or invalid webhook specified');
}
- if (!core.getInput('interval') && !core.getInput('cache_dir')) {
+ if (core.getInput('interval').length === 0 && core.getInput('cache_dir').length === 0) {
throw new Error('No interval or cache folder specified');
}
- if (core.getInput('interval') && parseInt(core.getInput('interval')).toString() === 'NaN') {
+ if (core.getInput('interval').length > 0 && parseInt(core.getInput('interval')).toString() === 'NaN') {
throw new Error('Invalid interval specified');
}
+
+ if (
+ core.getInput('unfurl').length > 0 &&
+ core.getBooleanInput('unfurl') &&
+ (core.getInput('show_desc').length > 0 ||
+ core.getInput('show_link').length > 0 ||
+ core.getInput('show_date').length > 0 ||
+ core.getInput('show_img').length > 0)
+ ) {
+ throw new Error('Unfurled links cannot be styled with `show` options');
+ }
};
export { validate };
diff --git a/src/types.d.ts b/src/types.d.ts
index 7411d76..8ce221f 100644
--- a/src/types.d.ts
+++ b/src/types.d.ts
@@ -30,15 +30,16 @@ interface Icons {
icons?: Icon[];
}
+/* eslint-disable camelcase */
interface Block {
type?: string;
- text?: {
- type?: string;
- text?: string;
- };
+ text?: string | Block;
+ fields?: Block[];
+ image_url?: string;
+ accessory?: Block;
+ elements?: Block[];
}
-/* eslint-disable camelcase */
interface Payload {
as_user?: boolean;
username?: string;