Skip to content

Commit

Permalink
✨ webhook notifications (#296)
Browse files Browse the repository at this point in the history
  • Loading branch information
juftin committed Sep 13, 2023
1 parent 383617a commit 3ab5333
Show file tree
Hide file tree
Showing 10 changed files with 206 additions and 5 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ camply campgrounds --search "Fire Lookout Towers" --state CA
Search for available campsites, get a notification whenever one becomes
available, and continue searching after the first one is found. The below command
is using `silent` notifications as an example but camply also supports `Email`,
`Slack`, `Twilio` (SMS), `Pushover`, `Pushbullet`, `Ntfy`, `Apprise`, and `Telegram`.
`Slack`, `Twilio` (SMS), `Pushover`, `Pushbullet`, `Ntfy`, `Apprise`, `Telegram`,
and `Webhook`.

```commandline
camply campsites \
Expand Down
2 changes: 2 additions & 0 deletions camply/config/file_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ class FileConfig:
"notes": "NTFY Notification Topic",
},
APPRISE_URL={"default": "", "notes": "Apprise notification URL"},
WEBHOOK_URL={"default": "", "notes": "Webhook URL"},
WEBHOOK_HEADERS={"default": "", "notes": "Webhook JSON Headers"},
RIDB_API_KEY={
"default": "",
"notes": "Personal Recreation.gov API Key (not required)",
Expand Down
15 changes: 14 additions & 1 deletion camply/config/notification_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
Project Configuration for Pushover Variables
"""

import json
import logging
from os import getenv
from typing import List, Optional
from typing import Any, Dict, List, Optional

from dotenv import load_dotenv

Expand Down Expand Up @@ -120,3 +121,15 @@ class TelegramConfig:
"parse_mode": "MarkdownV2",
"disable_web_page_preview": "true",
}


class WebhookConfig:
"""
Webhook Notification Config Class
"""

WEBHOOK_URL: Optional[str] = getenv("WEBHOOK_URL", None)
DEFAULT_HEADERS: Dict[str, Any] = {"Content-Type": "application/json"}
WEBHOOK_HEADERS: Dict[str, Any] = json.loads(
getenv("WEBHOOK_HEADERS", None) or "{}"
)
14 changes: 13 additions & 1 deletion camply/containers/data_containers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@

import datetime
import logging
from functools import partial
from typing import List, Optional, Tuple, Union

from pydantic import validator
from pydantic import Field, validator

from camply.containers.base_container import (
CamplyModel,
Expand Down Expand Up @@ -142,3 +143,14 @@ class ListedCampsite(CamplyModel):
name: str
id: int
facility_id: int


class WebhookBody(CamplyModel):
"""
Webhook Body
"""

campsites: List[AvailableCampsite]
timestamp: datetime.datetime = Field(
default_factory=partial(datetime.datetime.now, tz=datetime.timezone.utc)
)
2 changes: 2 additions & 0 deletions camply/notifications/base_notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ class BaseNotifications(ABC):
Base Notifications
"""

last_gasp: bool = True

def __init__(self) -> None:
"""
Instantiate with a Requests Session
Expand Down
5 changes: 4 additions & 1 deletion camply/notifications/multi_provider_notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from camply.notifications.slack import SlackNotifications
from camply.notifications.telegram import TelegramNotifications
from camply.notifications.twilio import TwilioNotifications
from camply.notifications.webhook import WebhookNotifications

logger = logging.getLogger(__name__)

Expand All @@ -29,6 +30,7 @@
"slack": SlackNotifications,
"telegram": TelegramNotifications,
"twilio": TwilioNotifications,
"webhook": WebhookNotifications,
"silent": SilentNotifications,
}

Expand Down Expand Up @@ -123,5 +125,6 @@ def last_gasp(self, error: Exception) -> None:
f"[{date_string}] - ({error.__class__.__name__}) {error_string}"
)
for provider in self.providers:
provider.send_message(error_message)
if provider.last_gasp is True:
provider.send_message(error_message)
raise RuntimeError(error_message) from error
74 changes: 74 additions & 0 deletions camply/notifications/webhook.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"""
Generic Webhook Notifications
"""

import logging
from typing import List

import requests

from camply.config.notification_config import WebhookConfig
from camply.containers import AvailableCampsite
from camply.containers.data_containers import WebhookBody
from camply.notifications.base_notifications import BaseNotifications

logger = logging.getLogger(__name__)


class WebhookNotifications(BaseNotifications):
"""
Push Notifications via Webhooks
"""

last_gasp: bool = False

def __init__(self):
super().__init__()
self.webhook_url = WebhookConfig.WEBHOOK_URL
self.webhook_headers = WebhookConfig.DEFAULT_HEADERS
self.webhook_headers.update(WebhookConfig.WEBHOOK_HEADERS)
self.session.headers = self.webhook_headers
if self.webhook_url is None:
warning_message = (
"Webhook notifications are not configured properly. "
"To send webhook messages "
"make sure to run `camply configure` or set the "
"proper environment variables: "
"`WEBHOOK_URL` / `WEBHOOK_HEADERS`."
)
logger.error(warning_message)
raise EnvironmentError(warning_message)

def send_message(self, message: str, **kwargs) -> requests.Response:
"""
Send a message via Webhook
Parameters
----------
message: str
Returns
-------
requests.Response
"""
response = self.session.post(url=self.webhook_url, data=message)
try:
response.raise_for_status()
except requests.HTTPError as he:
logger.warning(
f"Notifications weren't able to be sent to {self.webhook_url}. "
"Your configuration might be incorrect."
)
raise ConnectionError(response.text) from he
return response

def send_campsites(self, campsites: List[AvailableCampsite], **kwargs):
"""
Send a message with a campsite object
Parameters
----------
campsites: List[AvailableCampsite]
"""
webhook_body = WebhookBody(campsites=campsites).json()
self.send_message(message=webhook_body)
92 changes: 91 additions & 1 deletion docs/command_line_usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,8 @@ and a link to make the booking. Required parameters include `--start-date`, `--e
[\*\*_example_](#continue-looking-after-the-first-match-is-found)
- `--notifications`: `NOTIFICATIONS`
- Enables continuous searching. Types of notifications to receive. Options available
are `pushover`, `email`, `ntfy`, `apprise`, `pushbullet`, `slack`, `telegram`, `twilio`, `silent`.
are `pushover`, `email`, `ntfy`, `apprise`, `pushbullet`, `slack`, `telegram`, `twilio`,
`webhook`, `silent`.
Defaults to `silent` - which just logs messages to console.
[\*\*_example_](#send-a-push-notification)
- `--equipment`
Expand Down Expand Up @@ -997,3 +998,92 @@ camply campsites \
--search-once \
--offline-search
```

### Send a webhook notification

Camply supports sending notifications to a webhook URL.
This can be useful if you want to integrate camply with
services that accept webhooks, like [IFTTT](https://ifttt.com/).

Webhook notifications require the `WEBHOOK_URL` environment variable /
config value to be set. Optionally, you can also set the `WEBHOOK_HEADERS`
config value to a JSON object containing any headers you want to send with
the webhook POST request.

```commandline
export WEBHOOK_URL="https://webhook.site/api/webhook"
export WEBHOOK_HEADERS='{"Authorization": "Bearer 1234"}'
```

Unless explicitly set otherwise, `WEBHOOK_HEADERS` will have
`Content-Type: application/json` set.

```commandline
camply campsites \
--campground 232451 \
--start-date 2023-09-10 \
--end-date 2023-09-13 \
--notifications webhook
```

The JSON POST body is an array of available campsites
under a `campsites` key.

```json
{
"campsites": [
{
"campsite_id": 981,
"booking_date": "2023-09-12T00:00:00",
"booking_end_date": "2023-09-13T00:00:00",
"booking_nights": 1,
"campsite_site_name": "C",
"campsite_loop_name": "Tent Only Group Area",
"campsite_type": "GROUP TENT ONLY AREA NONELECTRIC",
"campsite_occupancy": [13, 30],
"campsite_use_type": "Overnight",
"availability_status": "Available",
"recreation_area": "Yosemite National Park, CA",
"recreation_area_id": 2991,
"facility_name": "Hodgdon Meadow Campground",
"facility_id": 232451,
"booking_url": "https://www.recreation.gov/camping/campsites/981",
"permitted_equipment": [
{
"equipment_name": "Tent",
"max_length": 0.0
},
{
"equipment_name": "Large Tent Over 9X12`",
"max_length": 0.0
},
{
"equipment_name": "Small Tent",
"max_length": 0.0
}
],
"campsite_attributes": [
{
"attribute_category": "site_details",
"attribute_id": 11,
"attribute_name": "Checkin Time",
"attribute_value": "12:00 PM"
},
{
"attribute_category": "site_details",
"attribute_id": 56,
"attribute_name": "Min Num of People",
"attribute_value": "13"
},
{
"attribute_category": "site_details",
"attribute_id": 9,
"attribute_name": "Campfire Allowed",
"attribute_value": "Yes"
}
]
}
],
"timestamp": "2023-09-10T01:57:00.729918+00:00"
}
```
3 changes: 3 additions & 0 deletions docs/how_to_run.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,9 @@ available.
- Telegram Notifications
- `TELEGRAM_BOT_TOKEN`
- `TELEGRAM_CHAT_ID`
- Webhook Notifications
- `WEBHOOK_URL`
- `WEBHOOK_HEADERS` (optional, defaults to `{"Content-Type": "application/json"}`)
- Optional Environment Variables
- `LOG_LEVEL` (sets logging level, defaults to "INFO")
- `PUSHOVER_PUSH_TOKEN` (Personal Pushover App Token)
Expand Down
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ to book your spot!
- [Using the Daily Providers](command_line_usage.md#using-the-daily-providers)
- [Search ReserveCalifornia](command_line_usage.md#search-reservecalifornia)
- [Run camply as a CRON Job](command_line_usage.md#run-camply-as-a-cron-job)
- [Send a Webhook Notification](command_line_usage.md#send-a-webhook-notification)
- [How to Run Camply](how_to_run.md#how-to-run-camply)
- [Run Modes](how_to_run.md#run-modes)
- [non-continuous](how_to_run.md#non-continuous)
Expand Down

0 comments on commit 3ab5333

Please sign in to comment.