Skip to content

Commit 894ba3f

Browse files
authored
Merge pull request #34 from TBosak/main
Implementing webhook configuration
2 parents d483f65 + 0fb734c commit 894ba3f

File tree

9 files changed

+974
-14
lines changed

9 files changed

+974
-14
lines changed

.gitignore

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ dist
166166
.yarn/unplugged
167167
.yarn/build-state.yml
168168
.yarn/install-state.gz
169-
.pnp.\*
169+
.pnp.*
170170
public/feeds/
171-
configs/
171+
configs/
172+
feed-history/

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -98,20 +98,20 @@ Some websites structure content across multiple layers:
9898

9999
## 🔧 To Do
100100

101-
- [ ] Add ALL possible RSS fields to models
101+
- [x] Add ALL possible RSS fields to models
102102
- [x] Add option for parallel iterators
103103
- [ ] Scraping how-to video
104104
- [x] Add feed preview pane
105105
- [ ] Store/compare feed data to enable timestamping feed items
106106
- [x] Create dockerfile
107107
- [ ] Create Helm chart files
108108
- [x] Create GUI
109-
- [ ] Utilities
109+
- [x] Utilities
110110
- [x] HTML stripper
111111
- [x] Source URL wrapper for relative links
112-
- [ ] Nested link follower/drilldown functionality for each feed item property
112+
- [x] Nested link follower/drilldown functionality for each feed item property
113113
- [x] Adjust date parser logic with overrides from an optional date format input
114-
- [ ] Add selector suggestion engine
114+
- [x] Add selector suggestion engine
115115
- [ ] Amass contributors
116116

117117
<br>

index.ts

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -364,11 +364,22 @@ app.post("/", async (ctx) => {
364364
user: extract("emailUsername"),
365365
encryptedPassword: encrypt(extract("emailPassword"), encryptionKey),
366366
folder: extract("emailFolder"),
367+
emailCount: parseInt(extract("emailCount", "10")) || 10,
367368
};
368369
feedOptions.feedLanguage = "en";
369370
feedOptions.feedDescription = `Emails from folder: ${emailConfigData.folder}`;
370371
}
371372

373+
// Webhook configuration
374+
const webhookConfig = {
375+
enabled: extractBool("webhookEnabled"),
376+
url: extract("webhookUrl", ""),
377+
format: extract("webhookFormat", "xml") as "xml" | "json",
378+
newItemsOnly: extractBool("webhookNewItemsOnly", true),
379+
headers: extractJson("webhookHeaders"),
380+
customPayload: extract("webhookCustomPayload", "").trim() || undefined,
381+
};
382+
372383
const finalFeedConfig = {
373384
feedId,
374385
feedName,
@@ -379,6 +390,7 @@ app.post("/", async (ctx) => {
379390
advanced: extractBool("advanced"),
380391
headers: extractJson("headers"),
381392
cookies: cookies.length > 0 ? cookies : undefined,
393+
webhook: webhookConfig.enabled && webhookConfig.url ? webhookConfig : undefined,
382394

383395
config: feedType === "email" ? emailConfigData : configData,
384396
...(feedType === "webScraping" && { article: articleData }),
@@ -647,6 +659,28 @@ app.get("/feeds", async (ctx) => {
647659
function confirmDelete(feedId) {
648660
return confirm("Are you sure you want to delete this feed?");
649661
}
662+
663+
async function triggerWebhook(feedId) {
664+
try {
665+
const response = await fetch('/trigger-webhook', {
666+
method: 'POST',
667+
headers: {
668+
'Content-Type': 'application/json',
669+
},
670+
body: JSON.stringify({ feedId })
671+
});
672+
673+
const result = await response.json();
674+
675+
if (response.ok) {
676+
alert('Webhook triggered successfully!\\n\\nFeed: ' + feedId + '\\nWebhook URL: ' + result.webhookUrl + '\\nItems sent: ' + result.itemCount);
677+
} else {
678+
alert('Webhook failed: ' + result.error);
679+
}
680+
} catch (error) {
681+
alert('Error triggering webhook: ' + error.message);
682+
}
683+
}
650684
</script>
651685
<header style="text-align:center;"><h1>Active RSS Feeds</h1></header>
652686
<div>
@@ -677,6 +711,7 @@ app.get("/feeds", async (ctx) => {
677711
}
678712

679713
// Build the card for this feed
714+
const hasWebhook = config.webhook?.enabled && config.webhook?.url;
680715
response += `
681716
<article>
682717
<header>
@@ -685,14 +720,20 @@ app.get("/feeds", async (ctx) => {
685720
<p><strong>Feed ID:</strong> ${feedId}</p>
686721
<p><strong>Build Time:</strong> ${lastBuildDate}</p>
687722
<p><strong>Feed Type:</strong> ${feedType}</p>
723+
${hasWebhook ? `<p><strong>Webhook:</strong> ✅ Enabled</p>` : '<p><strong>Webhook:</strong> ❌ Disabled</p>'}
688724
<footer>
689725
<div class="grid">
690726
<a href="public/feeds/${feedId}.xml" style="margin-right: auto;line-height:3em;">View Feed</a>
727+
${hasWebhook ? `
728+
<button onclick="triggerWebhook('${feedId}')" class="outline">
729+
🪝 Trigger Webhook
730+
</button>
731+
` : ''}
691732
<form action="/delete-feed" method="POST" style="display:inline;" onsubmit="return confirmDelete('${feedId}')">
692733
<input type="hidden" name="feedId" value="${feedId}">
693734
<button type="submit" style="width:25%;margin-left:auto;float:right;" class="outline contrast">Delete</button>
735+
</form>
694736
</div>
695-
</form>
696737
</footer>
697738
</article>
698739
`;
@@ -827,6 +868,66 @@ app.post("/delete-feed", async (c) => {
827868
}
828869
});
829870

871+
app.post("/trigger-webhook", async (c) => {
872+
const { feedId } = await c.req.json();
873+
874+
if (!feedId) {
875+
return c.json({ error: "Feed ID is required" }, 400);
876+
}
877+
878+
try {
879+
const sanitizedFeedId = basename(feedId as string);
880+
const configPath = join(configsDir, `${sanitizedFeedId}.yaml`);
881+
882+
// Check if feed exists
883+
if (!existsSync(configPath)) {
884+
return c.json({ error: "Feed not found" }, 404);
885+
}
886+
887+
// Load feed configuration
888+
const yamlContent = await readFile(configPath, "utf8");
889+
const feedConfig = yaml.load(yamlContent) as any;
890+
891+
if (!feedConfig.webhook?.enabled || !feedConfig.webhook?.url) {
892+
return c.json({ error: "Webhook not configured for this feed" }, 400);
893+
}
894+
895+
// Read current RSS feed
896+
const rssPath = join(feedPath, `${sanitizedFeedId}.xml`);
897+
if (!existsSync(rssPath)) {
898+
return c.json({ error: "RSS feed not generated yet" }, 404);
899+
}
900+
901+
const rssXml = await readFile(rssPath, "utf8");
902+
903+
// Import webhook utilities
904+
const { sendWebhook, createWebhookPayload, createJsonWebhookPayload } = await import("./utilities/webhook.utility");
905+
906+
// Create webhook payload
907+
const payload = feedConfig.webhook.format === "json"
908+
? createJsonWebhookPayload(feedConfig, rssXml, "manual")
909+
: createWebhookPayload(feedConfig, rssXml, "manual");
910+
911+
// Send webhook
912+
const success = await sendWebhook(feedConfig.webhook, payload);
913+
914+
if (success) {
915+
return c.json({
916+
message: "Webhook triggered successfully",
917+
feedId: sanitizedFeedId,
918+
webhookUrl: feedConfig.webhook.url,
919+
itemCount: payload.itemCount
920+
});
921+
} else {
922+
return c.json({ error: "Failed to send webhook" }, 500);
923+
}
924+
925+
} catch (error) {
926+
console.error("Error triggering webhook:", error);
927+
return c.json({ error: "Internal server error" }, 500);
928+
}
929+
});
930+
830931
app.post("/imap/folders", async (c) => {
831932
const config = await c.req.json<Config>();
832933
console.log("IMAP config:", config);

node/imap-watch.utility.ts

Lines changed: 112 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import Imap from "node-imap";
55
import libmime from "libmime";
66
import minimist from "minimist";
77
import RSS from "rss";
8-
import { existsSync, readFileSync, writeFileSync } from "fs";
8+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
99
import { simpleParser } from "mailparser";
1010
import { decrypt } from "../utilities/security.utility.ts";
1111
import { fileURLToPath } from "url";
@@ -62,6 +62,7 @@ interface RSSItemOptions {
6262
}
6363

6464
interface RSSFeedOptions {
65+
webhook: any;
6566
title: string; // Title of the feed
6667
description: string; // Description of the feed
6768
feed_url: string; // URL of the RSS feed itself (where it will be published)
@@ -207,7 +208,7 @@ class ImapWatcher {
207208
this.fetchRecentStartupEmails();
208209

209210
this.imap.on("mail", (n) => {
210-
console.log(`[IMAP] New mail event: ${n}`);
211+
console.log(`[IMAP] New mail event received for feed ${this.config.feedId}: ${n} new email(s)`);
211212
this.fetchNewEmails();
212213
});
213214

@@ -438,15 +439,23 @@ class ImapWatcher {
438439
.map((result) => (result as PromiseFulfilledResult<Email>).value);
439440

440441
if (emails.length > 0) {
441-
console.log("[IMAP] Recent emails fetched, updating RSS...");
442+
console.log(`[IMAP] Recent emails fetched for feed ${this.config.feedId}, updating RSS with ${emails.length} emails...`);
442443
const rss = buildRSSFromEmailFolder(emails, this.config);
443444
writeFileSync(
444445
path.join(__dirname, "../public/feeds", `${this.config.feedId}.xml`),
445446
rss,
446447
);
447-
console.log("[IMAP] RSS Feed regenerated");
448+
console.log(`[IMAP] RSS Feed regenerated for feed ${this.config.feedId}`);
449+
450+
// Handle webhook if configured
451+
if (this.config.webhook?.enabled && this.config.webhook?.url) {
452+
console.log(`[IMAP] Calling webhook handler for feed ${this.config.feedId}`);
453+
this.handleWebhook(rss);
454+
} else {
455+
console.log(`[IMAP] Webhook not configured for feed ${this.config.feedId} - enabled: ${this.config.webhook?.enabled}, url: ${!!this.config.webhook?.url}`);
456+
}
448457
} else {
449-
console.log("[IMAP] No valid emails found.");
458+
console.log(`[IMAP] No valid emails found for feed ${this.config.feedId}`);
450459
}
451460
});
452461
console.log("[IMAP] Completed processing new emails.");
@@ -469,6 +478,97 @@ class ImapWatcher {
469478
this.imap.end();
470479
}
471480
}
481+
482+
private async handleWebhook(rssXml: string): Promise<void> {
483+
try {
484+
console.log(`[IMAP] Webhook handler called for feed ${this.config.feedId}`);
485+
486+
if (!this.config.webhook?.enabled || !this.config.webhook?.url) {
487+
console.log(`[IMAP] Webhook not configured for feed ${this.config.feedId} - enabled: ${this.config.webhook?.enabled}, url: ${!!this.config.webhook?.url}`);
488+
return;
489+
}
490+
491+
console.log(`[IMAP] Webhook configured for feed ${this.config.feedId} - URL: ${this.config.webhook.url}, format: ${this.config.webhook.format}, newItemsOnly: ${this.config.webhook.newItemsOnly}`);
492+
493+
let shouldSendWebhook = true;
494+
let webhookRssXml = rssXml;
495+
496+
// Check if only new items should be sent
497+
if (this.config.webhook.newItemsOnly) {
498+
const historyPath = path.join(__dirname, "../feed-history", `${this.config.feedId}.xml`);
499+
let previousRss = null;
500+
501+
try {
502+
if (existsSync(historyPath)) {
503+
previousRss = readFileSync(historyPath, "utf8");
504+
}
505+
} catch (err) {
506+
console.warn("[IMAP] Could not read feed history:", err.message);
507+
}
508+
509+
// Use centralized new item detection
510+
try {
511+
const { getNewItemsFromRSS } = await import("../utilities/webhook.utility.ts");
512+
const newItemsRss = getNewItemsFromRSS(rssXml, previousRss);
513+
514+
if (!newItemsRss) {
515+
shouldSendWebhook = false;
516+
console.log(`[IMAP] No new items detected for feed ${this.config.feedId}, skipping webhook`);
517+
} else {
518+
webhookRssXml = newItemsRss;
519+
console.log(`[IMAP] New items detected for feed ${this.config.feedId}, will send webhook`);
520+
}
521+
} catch (importErr) {
522+
console.warn("[IMAP] Could not import webhook utilities, falling back to simple comparison:", importErr.message);
523+
// Fallback to simple comparison
524+
if (previousRss && previousRss.trim() === rssXml.trim()) {
525+
shouldSendWebhook = false;
526+
console.log("[IMAP] No changes detected, skipping webhook");
527+
}
528+
}
529+
}
530+
531+
if (shouldSendWebhook) {
532+
try {
533+
// Use centralized webhook system
534+
const { sendWebhook, createWebhookPayload, createJsonWebhookPayload } = await import("../utilities/webhook.utility.ts");
535+
536+
const payload = this.config.webhook.format === "json"
537+
? createJsonWebhookPayload(this.config, webhookRssXml, "automatic")
538+
: createWebhookPayload(this.config, webhookRssXml, "automatic");
539+
540+
const success = await sendWebhook(this.config.webhook, payload);
541+
542+
if (success) {
543+
console.log(`[IMAP] Webhook sent successfully for feed ${this.config.feedId} to ${this.config.webhook.url}`);
544+
545+
// Store current RSS for future comparison using centralized history utility
546+
try {
547+
const { storeFeedHistory } = await import("../utilities/feed-history.utility.ts");
548+
await storeFeedHistory(this.config.feedId, rssXml);
549+
console.log(`[IMAP] Feed history stored for feed ${this.config.feedId}`);
550+
} catch (historyErr) {
551+
console.warn(`[IMAP] Could not store feed history for feed ${this.config.feedId}:`, historyErr.message);
552+
// Fallback to manual file storage
553+
const historyDir = path.join(__dirname, "../feed-history");
554+
if (!existsSync(historyDir)) {
555+
mkdirSync(historyDir, { recursive: true });
556+
}
557+
writeFileSync(path.join(historyDir, `${this.config.feedId}.xml`), rssXml, "utf8");
558+
console.log(`[IMAP] Feed history stored manually for feed ${this.config.feedId}`);
559+
}
560+
} else {
561+
console.warn(`[IMAP] Webhook failed for feed ${this.config.feedId} to ${this.config.webhook.url}`);
562+
}
563+
} catch (webhookErr) {
564+
console.error(`[IMAP] Error using centralized webhook system:`, webhookErr.message);
565+
}
566+
}
567+
} catch (error) {
568+
console.error(`[IMAP] Webhook error:`, error.message);
569+
}
570+
}
571+
472572
}
473573

474574
export function buildRSSFromEmailFolder(emails: Email[], feedSetup: RSSFeedOptions): string {
@@ -591,9 +691,16 @@ const completeFeedConfig: RSSFeedOptions = {
591691
feedImage: rawConfig.feedImage,
592692
generator: rawConfig.generator,
593693
config: imapOriginalConfig,
694+
webhook: rawConfig.webhook, // Pass through webhook configuration
594695
};
595696

596697
console.log("[IMAP Node Watcher] ImapWatcher will attempt to connect to host:", completeFeedConfig.config?.host, "port:", completeFeedConfig.config?.port);
698+
console.log("[IMAP Node Watcher] Webhook configuration:", JSON.stringify({
699+
enabled: completeFeedConfig.webhook?.enabled,
700+
url: completeFeedConfig.webhook?.url ? '[REDACTED]' : undefined,
701+
format: completeFeedConfig.webhook?.format,
702+
newItemsOnly: completeFeedConfig.webhook?.newItemsOnly
703+
}, null, 2));
597704

598705
const watcher = new ImapWatcher(completeFeedConfig);
599706

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "mkfd",
33
"module": "index.ts",
44
"type": "module",
5-
"version": "2.0.0",
5+
"version": "2.0.1",
66
"scripts": {
77
"start": "bun run index.ts",
88
"dev": "bun run index.ts --watch --passkey=admin123 --cookieSecret=a18c1fd2211edd76a18c1fd2211edd76 --encryptionKey=a18c1fd2211edd76"

0 commit comments

Comments
 (0)