Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .DS_Store
Binary file not shown.
38 changes: 35 additions & 3 deletions xExtension-Webhook/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,13 @@ A powerful FreshRSS extension that automatically sends webhook notifications whe
- **Multiple HTTP Methods**: Supports GET, POST, PUT, DELETE, PATCH, OPTIONS, and HEAD
- **Configurable Formats**: Send data as JSON or form-encoded
- **Template System**: Customizable webhook payloads with placeholders
- **Comprehensive Logging**: Detailed logging for debugging and monitoring
- **Update-aware Filters**: Ignore updates to existing entries to avoid duplicate notifications
- **Two Matching Modes**: Keep simple global matching or switch to field-specific advanced matching
- **Comprehensive Logging**: Optional, privacy-aware logging for debugging and monitoring
- **Test Functionality**: Built-in “Save and send test request” button to verify configuration instantly
- **Thumbnail Placeholder**: Populate payloads with `__THUMBNAIL_URL__` for embeds and cards
- **Plaintext Content**: Use `__CONTENT_PLAINTEXT__` for trimmed, tag-free notifications
- **Error Handling**: Robust error handling with graceful fallbacks
- **Test Functionality**: Built-in test feature to verify webhook configuration

## 📋 Requirements

Expand Down Expand Up @@ -51,14 +55,26 @@ your-project-name
- **Search in Feed**: Match keywords in feed names
- **Search in Authors**: Match keywords in author names
- **Search in Content**: Match keywords in article content
- **Ignore Updated Entries**: Skip webhooks when an existing entry is updated
- **Mark as Read**: Automatically mark matched entries as read after the webhook is sent

#### Matching Modes

- **Basic Matching**: One keyword list, plus shared search scopes (title/feed/authors/content)
- **Advanced Matching**: Four dedicated keyword lists so each field has its own patterns

#### Webhook Settings

- **Webhook URL**: Your webhook endpoint URL
- **HTTP Method**: Choose from GET, POST, PUT, DELETE, etc.
- **HTTP Method**: Choose from GET, POST, PUT, DELETE, PATCH, OPTIONS, or HEAD
- **Body Type**: JSON or Form-encoded
- **Headers**: Custom HTTP headers (one per line)

### Logging & Testing

- **Enable Logging**: Toggle HTTP request/response logging (writes to FreshRSS logs)
- **Save and Send Test Request**: Saves the configuration and immediately sends a test payload using the provided settings

### Webhook Body Template

Customize the webhook payload using placeholders:
Expand All @@ -84,10 +100,12 @@ Customize the webhook payload using placeholders:
| `__FEED__` | Feed name |
| `__URL__` | Article URL |
| `__CONTENT__` | Article content |
| `__CONTENT_PLAINTEXT__` | HTML-free excerpt (trimmed to ~360 characters) |
| `__DATE__` | Publication date |
| `__DATE_TIMESTAMP__` | Unix timestamp |
| `__AUTHORS__` | Article authors |
| `__TAGS__` | Article tags |
| `__THUMBNAIL_URL__` | First enclosure thumbnail or article thumbnail |

## 🎯 Use Cases

Expand Down Expand Up @@ -205,6 +223,20 @@ Enable logging in the extension settings to see detailed information about:

## 📝 Changelog

### Version 0.3.0

- Added matching modes: `Basic` (existing behavior) and `Advanced` (field-specific keyword lists)
- Added advanced keyword groups for title, feed, authors, and content
- Improved configuration model to support both matching strategies cleanly
- Updated configuration UI and documentation for the new matching workflow

### Version 0.2.0

- Added `__CONTENT_PLAINTEXT__` placeholder (HTML stripped and length-limited for notifications)
- Added support for `__THUMBNAIL_URL__` placeholder
- Improved webhook request handling for headers, methods, and body encoding
- Added options for "Ignore updated entries" and explicit logging toggle

### Version 0.1.1

- Initial release
Expand Down
94 changes: 83 additions & 11 deletions xExtension-Webhook/configure.phtml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,26 @@ declare(strict_types=1);

<fieldset>
<legend><?= _t('ext.webhook.event_settings') ?> ⚙️</legend>
<details open="open">
<summary class="stick"><small><?= _t('ext.webhook.show_hide') ?></small></summary>
<details open="open">
<summary class="stick"><small><?= _t('ext.webhook.show_hide') ?></small></summary>

<div class="form-group">
<label class="group-name" for="match_mode"><?= _t('ext.webhook.match_mode') ?></label>
<div class="group-controls">
<select name="match_mode" id="match_mode">
<option value="basic" <?= $this->getMatchMode() === 'basic' ? 'selected' : '' ?>>
<?= _t('ext.webhook.match_mode_basic') ?>
</option>
<option value="advanced" <?= $this->getMatchMode() === 'advanced' ? 'selected' : '' ?>>
<?= _t('ext.webhook.match_mode_advanced') ?>
</option>
</select>
<br />
<small><?= _t('ext.webhook.match_mode_description') ?></small>
</div>
</div>

<h3><?= _t('ext.webhook.basic_matching') ?></h3>

<div class="form-group">
<label class="group-name" for="keywords"><?= _t('ext.webhook.keywords') ?></label>
Expand All @@ -25,8 +43,8 @@ declare(strict_types=1);
</div>

<!-- Search in article's: -->
<div class="form-group">
<label class="group-name" for="search_in"><?= _t('ext.webhook.search_in') ?></label>
<div class="form-group">
<label class="group-name" for="search_in"><?= _t('ext.webhook.search_in') ?></label>
<div class="group-controls">
<table>
<tr>
Expand All @@ -52,17 +70,57 @@ declare(strict_types=1);
</td>
</tr>
</table>
</div>
</div>
</div>

<div class="form-group">
<label class="group-name" for="mark_as_read"><?= _t('ext.webhook.mark_as_read') ?></label>
<h3><?= _t('ext.webhook.advanced_matching') ?></h3>
<small><?= _t('ext.webhook.advanced_matching_description') ?></small>

<div class="form-group">
<label class="group-name" for="keywords_title"><?= _t('ext.webhook.advanced_title') ?></label>
<div class="group-controls">
<textarea name="keywords_title" id="keywords_title"><?= $this->getKeywordDataByField('title') ?></textarea>
</div>
</div>

<div class="form-group">
<label class="group-name" for="keywords_feed"><?= _t('ext.webhook.advanced_feed') ?></label>
<div class="group-controls">
<textarea name="keywords_feed" id="keywords_feed"><?= $this->getKeywordDataByField('feed') ?></textarea>
</div>
</div>

<div class="form-group">
<label class="group-name" for="keywords_authors"><?= _t('ext.webhook.advanced_authors') ?></label>
<div class="group-controls">
<textarea name="keywords_authors" id="keywords_authors"><?= $this->getKeywordDataByField('authors') ?></textarea>
</div>
</div>

<div class="form-group">
<label class="group-name" for="keywords_content"><?= _t('ext.webhook.advanced_content') ?></label>
<div class="group-controls">
<textarea name="keywords_content" id="keywords_content"><?= $this->getKeywordDataByField('content') ?></textarea>
</div>
</div>

<div class="form-group">
<label class="group-name" for="mark_as_read"><?= _t('ext.webhook.mark_as_read') ?></label>
<div class="group-controls">
<input type="checkbox" name="mark_as_read" id="mark_as_read" value="1"
<?= FreshRSS_Context::userConf()->attributeBool('mark_as_read') ? 'checked="checked"' : '' ?> />
</div>
</div>

<div class="form-group">
<label class="group-name" for="ignore_updated"><?= _t('ext.webhook.ignore_updated') ?></label>
<div class="group-controls">
<input type="checkbox" name="ignore_updated" id="ignore_updated" value="1"
<?= FreshRSS_Context::userConf()->attributeBool('ignore_updated') ? 'checked="checked"' : '' ?> />
<small><?= _t('ext.webhook.ignore_updated_description') ?></small>
</div>
</div>

</details>
</fieldset>

Expand All @@ -78,9 +136,8 @@ declare(strict_types=1);
<div class="group-controls">
<select id="webhook_method" name="webhook_method">
<?php
$currentMethod = FreshRSS_Context::userConf()->attributeString('webhook_method') ?? $this->webhook_method->value;
$supportedMethods = [HTTP_METHOD::POST, HTTP_METHOD::GET, HTTP_METHOD::PUT];
foreach ($supportedMethods as $method): ?>
$currentMethod = FreshRSS_Context::userConf()->attributeString('webhook_method') ?? HTTP_METHOD::POST->value;
foreach (HTTP_METHOD::cases() as $method): ?>
<option value="<?= $method->value ?>" <?= $currentMethod === $method->value ? 'selected' : '' ?>><?= $method->value ?></option>
<?php endforeach; ?>
</select>
Expand Down Expand Up @@ -120,6 +177,10 @@ declare(strict_types=1);
<td><code>__CONTENT__</code></td>
<td><?= _t('ext.webhook.http_body_placeholder_content_description') ?></td>
</tr>
<tr>
<td><code>__CONTENT_PLAINTEXT__</code></td>
<td><?= _t('ext.webhook.http_body_placeholder_plaintext_description') ?></td>
</tr>
<tr>
<td><code>__DATE__</code></td>
<td><?= _t('ext.webhook.http_body_placeholder_date_description') ?></td>
Expand Down Expand Up @@ -182,6 +243,15 @@ declare(strict_types=1);
</details>
</div>
</div>

<div class="form-group">
<label class="group-name" for="enable_logging"><?= _t('ext.webhook.enable_logging') ?></label>
<div class="group-controls">
<input type="checkbox" name="enable_logging" id="enable_logging" value="1"
<?= FreshRSS_Context::userConf()->attributeBool('enable_logging') ? 'checked="checked"' : '' ?> />
<small><?= _t('ext.webhook.enable_logging_description') ?></small>
</div>
</div>
</details>

</details>
Expand All @@ -192,7 +262,9 @@ declare(strict_types=1);
<div class="group-controls">
<button type="submit" class="btn btn-important"><?= _t('gen.action.submit') ?></button>
<button type="reset" class="btn"><?= _t('gen.action.cancel') ?></button>
<button type="menu" class="btn"><?= _t('ext.webhook.save_and_send_test_req') ?></button>
<button type="submit" name="test_request" value="1" class="btn">
<?= _t('ext.webhook.save_and_send_test_req') ?>
</button>
</div>
</div>

Expand Down
Loading