Merge pull request #405 from arabcoders/dev
Some checks failed
Build Native wrappers / build (amd64, ubuntu-latest) (push) Has been cancelled
Build Native wrappers / build (amd64, windows-latest) (push) Has been cancelled
Build Native wrappers / build (arm64, macos-latest) (push) Has been cancelled
Build Native wrappers / build (arm64, ubuntu-latest) (push) Has been cancelled
Build Native wrappers / build (arm64, windows-latest) (push) Has been cancelled

FEAT: Make it possible to limit notify target to specific presets.
This commit is contained in:
Abdulmohsen 2025-09-02 22:59:22 +03:00 committed by GitHub
commit c528e2b557
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 197 additions and 76 deletions

View file

@ -494,7 +494,12 @@ class DownloadQueue(metaclass=Singleton):
dlInfo.info.status = "not_live" dlInfo.info.status = "not_live"
dlInfo.info.msg = nMessage.replace(f" '{dlInfo.info.title}'", "") dlInfo.info.msg = nMessage.replace(f" '{dlInfo.info.title}'", "")
await self._notify.emit(Events.LOG_INFO, data={"lowPriority": True}, title=nTitle, message=nMessage) await self._notify.emit(
Events.LOG_INFO,
data={"preset": dlInfo.info.preset, "lowPriority": True},
title=nTitle,
message=nMessage,
)
itemDownload: Download = self.done.put(dlInfo) itemDownload: Download = self.done.put(dlInfo)
elif len(entry.get("formats", [])) < 1: elif len(entry.get("formats", [])) < 1:
@ -511,7 +516,12 @@ class DownloadQueue(metaclass=Singleton):
dlInfo.info.status = "error" dlInfo.info.status = "error"
itemDownload = self.done.put(dlInfo) itemDownload = self.done.put(dlInfo)
await self._notify.emit(Events.LOG_WARNING, data={"logs": text_logs}, title=nTitle, message=nMessage) await self._notify.emit(
Events.LOG_WARNING,
data={"preset": dlInfo.info.preset, "logs": text_logs},
title=nTitle,
message=nMessage,
)
elif is_premiere and self.config.prevent_live_premiere: elif is_premiere and self.config.prevent_live_premiere:
nStore = "history" nStore = "history"
nTitle = "Premiere Video" nTitle = "Premiere Video"
@ -546,7 +556,9 @@ class DownloadQueue(metaclass=Singleton):
nStore = "history" nStore = "history"
nEvent = Events.ITEM_MOVED nEvent = Events.ITEM_MOVED
nTitle = "Premiering right now" nTitle = "Premiering right now"
await self._notify.emit(Events.LOG_INFO, title=nTitle, message=nMessage) await self._notify.emit(
Events.LOG_INFO, data={"preset": dlInfo.info.preset}, title=nTitle, message=nMessage
)
else: else:
nEvent = Events.ITEM_ADDED nEvent = Events.ITEM_ADDED
nTitle = "Item Added" nTitle = "Item Added"
@ -559,7 +571,9 @@ class DownloadQueue(metaclass=Singleton):
await self._notify.emit( await self._notify.emit(
nEvent, nEvent,
data={"to": nStore, "item": itemDownload.info} if Events.ITEM_MOVED == nEvent else itemDownload.info, data={"to": nStore, "preset": itemDownload.info.preset, "item": itemDownload.info}
if Events.ITEM_MOVED == nEvent
else itemDownload.info,
title=nTitle, title=nTitle,
message=nMessage, message=nMessage,
) )
@ -664,7 +678,9 @@ class DownloadQueue(metaclass=Singleton):
if item.is_archived(): if item.is_archived():
message: str = f"The URL '{item.url}' is already downloaded and recorded in archive." message: str = f"The URL '{item.url}' is already downloaded and recorded in archive."
LOG.error(message) LOG.error(message)
await self._notify.emit(Events.LOG_INFO, title="Already Downloaded", message=message) await self._notify.emit(
Events.LOG_INFO, data={"preset": item.preset}, title="Already Downloaded", message=message
)
return {"status": "error", "msg": message} return {"status": "error", "msg": message}
started: float = time.perf_counter() started: float = time.perf_counter()
@ -777,7 +793,7 @@ class DownloadQueue(metaclass=Singleton):
self.done.put(item) self.done.put(item)
await self._notify.emit( await self._notify.emit(
Events.ITEM_MOVED, Events.ITEM_MOVED,
data={"to": "history", "item": item.info}, data={"to": "history", "preset": item.info.preset, "item": item.info},
title="Download Cancelled", title="Download Cancelled",
message=f"Download '{item.info.title}' has been cancelled.", message=f"Download '{item.info.title}' has been cancelled.",
) )
@ -1014,7 +1030,7 @@ class DownloadQueue(metaclass=Singleton):
_tasks.append( _tasks.append(
self._notify.emit( self._notify.emit(
Events.ITEM_MOVED, Events.ITEM_MOVED,
data={"to": "history", "item": entry.info}, data={"to": "history", "preset": entry.info.preset, "item": entry.info},
title=nTitle, title=nTitle,
message=nMessage, message=nMessage,
) )

View file

@ -15,7 +15,8 @@ from .BackgroundWorker import BackgroundWorker
from .config import Config from .config import Config
from .encoder import Encoder from .encoder import Encoder
from .Events import Event, EventBus, Events from .Events import Event, EventBus, Events
from .ItemDTO import ItemDTO from .ItemDTO import Item, ItemDTO
from .Presets import Preset, Presets
from .Singleton import Singleton from .Singleton import Singleton
from .Utils import validate_uuid from .Utils import validate_uuid
@ -71,7 +72,8 @@ class Target:
id: str id: str
name: str name: str
on: list[str] on: list[str] = field(default_factory=list)
presets: list[str] = field(default_factory=list)
request: TargetRequest request: TargetRequest
def serialize(self) -> dict: def serialize(self) -> dict:
@ -79,6 +81,7 @@ class Target:
"id": self.id, "id": self.id,
"name": self.name, "name": self.name,
"on": self.on, "on": self.on,
"presets": self.presets,
"request": self.request.serialize(), "request": self.request.serialize(),
} }
@ -144,11 +147,11 @@ class Notification(metaclass=Singleton):
Notification._instance = self Notification._instance = self
config: Config = config or Config.get_instance() config: Config = config or Config.get_instance()
self._debug = config.debug self._debug: bool = config.debug
self._file: Path = Path(file) if file else Path(config.config_path).joinpath("notifications.json") self._file: Path = Path(file) if file else Path(config.config_path).joinpath("notifications.json")
self._client: httpx.AsyncClient = client or httpx.AsyncClient() self._client: httpx.AsyncClient = client or httpx.AsyncClient()
self._encoder: Encoder = encoder or Encoder() self._encoder: Encoder = encoder or Encoder()
self._version = config.app_version self._version: str = config.app_version
self._offload: BackgroundWorker = background_worker or BackgroundWorker.get_instance() self._offload: BackgroundWorker = background_worker or BackgroundWorker.get_instance()
if self._file.exists() and "600" != self._file.stat().st_mode: if self._file.exists() and "600" != self._file.stat().st_mode:
@ -212,7 +215,7 @@ class Notification(metaclass=Singleton):
if not self._file.exists() or self._file.stat().st_size < 1: if not self._file.exists() or self._file.stat().st_size < 1:
return self return self
targets = [] targets: list = []
try: try:
LOG.info(f"Loading '{self._file}'.") LOG.info(f"Loading '{self._file}'.")
@ -255,6 +258,7 @@ class Notification(metaclass=Singleton):
id=target.get("id"), id=target.get("id"),
name=target.get("name"), name=target.get("name"),
on=target.get("on", []), on=target.get("on", []),
presets=target.get("presets", []),
request=TargetRequest( request=TargetRequest(
type=target.get("request", {}).get("type", "json"), type=target.get("request", {}).get("type", "json"),
method=target.get("request", {}).get("method", "POST"), method=target.get("request", {}).get("method", "POST"),
@ -317,8 +321,8 @@ class Notification(metaclass=Singleton):
msg = "Invalid notification target. Invalid 'on' event list found." msg = "Invalid notification target. Invalid 'on' event list found."
raise ValueError(msg) raise ValueError(msg)
removed_events = [] removed_events: list = []
all_events = NotificationEvents.get_events().values() all_events: dict[str, str] = NotificationEvents.get_events().values()
for e in target["on"]: for e in target["on"]:
if e not in all_events: if e not in all_events:
removed_events.append(e) removed_events.append(e)
@ -329,6 +333,24 @@ class Notification(metaclass=Singleton):
msg: str = f"Invalid notification target. Invalid events '{', '.join(removed_events)}' found." msg: str = f"Invalid notification target. Invalid events '{', '.join(removed_events)}' found."
raise ValueError(msg) raise ValueError(msg)
if "presets" in target:
if not isinstance(target["presets"], list):
msg = "Invalid notification target. Invalid 'presets' list found."
raise ValueError(msg)
removed_presets: list = []
all_presets: list[Preset] = Presets.get_instance().get_all()
for p in target["presets"]:
if p not in [ap.name for ap in all_presets]:
removed_presets.append(p)
target["presets"].remove(p)
continue
if len(removed_presets) > 0 and len(target["presets"]) < 1:
msg: str = f"Invalid notification target. Invalid presets '{', '.join(removed_presets)}' found."
raise ValueError(msg)
if "headers" in target["request"]: if "headers" in target["request"]:
if not isinstance(target["request"]["headers"], list): if not isinstance(target["request"]["headers"], list):
msg = "Invalid notification target. Invalid headers list found." msg = "Invalid notification target. Invalid headers list found."
@ -360,6 +382,9 @@ class Notification(metaclass=Singleton):
if len(target.on) > 0 and ev.event not in target.on and "test" != ev.event: if len(target.on) > 0 and ev.event not in target.on and "test" != ev.event:
continue continue
if "test" != ev.event and not self._check_preset(target, ev):
continue
if not target.request.url.startswith("http"): if not target.request.url.startswith("http"):
apprise_targets.append(target) apprise_targets.append(target)
else: else:
@ -373,6 +398,29 @@ class Notification(metaclass=Singleton):
return tasks return tasks
def _check_preset(self, target: Target, ev: Event) -> bool:
if len(target.presets) < 1:
return True
if not isinstance(ev.data, (Item, ItemDTO, dict)):
return False
preset_name: str | None = None
if isinstance(ev.data, Item):
preset_name = ev.data.preset
if isinstance(ev.data, ItemDTO):
preset_name = ev.data.preset
if isinstance(ev.data, dict):
preset_name = ev.data.get("preset", None)
if not preset_name:
return False
return preset_name in target.presets
async def _apprise(self, target: list[Target], ev: Event) -> dict: async def _apprise(self, target: list[Target], ev: Event) -> dict:
if not target or not isinstance(target, list): if not target or not isinstance(target, list):
return {} return {}
@ -406,7 +454,7 @@ class Notification(metaclass=Singleton):
try: try:
LOG.info(f"Sending Notification event '{ev.event}: {ev.id}' to '{target.name}'.") LOG.info(f"Sending Notification event '{ev.event}: {ev.id}' to '{target.name}'.")
reqBody = { reqBody: dict[str, Any] = {
"method": target.request.method.upper(), "method": target.request.method.upper(),
"url": target.request.url, "url": target.request.url,
"headers": { "headers": {
@ -423,7 +471,7 @@ class Notification(metaclass=Singleton):
for h in target.request.headers: for h in target.request.headers:
reqBody["headers"][h.key] = h.value reqBody["headers"][h.key] = h.value
body_key = "json" if "json" == target.request.type.lower() else "data" body_key: str = "json" if "json" == target.request.type.lower() else "data"
reqBody[body_key] = self._deep_unpack(ev.serialize()) reqBody[body_key] = self._deep_unpack(ev.serialize())
if "data" != target.request.data_key: if "data" != target.request.data_key:
@ -437,11 +485,15 @@ class Notification(metaclass=Singleton):
response = await self._client.request(**reqBody) response = await self._client.request(**reqBody)
respData = {"url": target.request.url, "status": response.status_code, "text": response.text} respData: dict[str, Any] = {
"url": target.request.url,
"status": response.status_code,
"text": response.text,
}
msg = f"Notification target '{target.name}' Responded to event '{ev.event}: {ev.id}' with status '{response.status_code}'." msg: str = f"Notification target '{target.name}' Responded to event '{ev.event}: {ev.id}' with status '{response.status_code}'."
if self._debug and respData.get("text"): if self._debug and respData.get("text"):
msg += f" body '{respData.get('text','??')}'." msg += f" body '{respData.get('text', '??')}'."
LOG.info(msg) LOG.info(msg)
@ -449,7 +501,7 @@ class Notification(metaclass=Singleton):
except Exception as e: except Exception as e:
err_msg = str(e) err_msg = str(e)
if not err_msg: if not err_msg:
err_msg = type(e).__name__ err_msg: str = type(e).__name__
LOG.error(f"Error sending Notification event '{ev.event}: {ev.id}' to '{target.name}'. '{err_msg!s}'.") LOG.error(f"Error sending Notification event '{ev.event}: {ev.id}' to '{target.name}'. '{err_msg!s}'.")
return {"url": target.request.url, "status": 500, "text": str(ev)} return {"url": target.request.url, "status": 500, "text": str(ev)}

View file

@ -434,13 +434,13 @@ class Tasks(metaclass=Singleton):
_tasks = [ _tasks = [
self._notify.emit( self._notify.emit(
Events.TASK_DISPATCHED, Events.TASK_DISPATCHED,
data=status, data={**status, "preset": task.preset} if status else {"preset": task.preset},
title=f"Task '{task.name}' dispatched", title=f"Task '{task.name}' dispatched",
message=f"Task '{task.name}' dispatched at '{timeNow}'.", message=f"Task '{task.name}' dispatched at '{timeNow}'.",
), ),
self._notify.emit( self._notify.emit(
Events.LOG_SUCCESS, Events.LOG_SUCCESS,
data={"lowPriority": True}, data={"preset": task.preset, "lowPriority": True},
title="Task completed", title="Task completed",
message=f"Task '{task.name}' completed in '{ended - started:.2f}'.", message=f"Task '{task.name}' completed in '{ended - started:.2f}'.",
), ),
@ -451,6 +451,7 @@ class Tasks(metaclass=Singleton):
LOG.error(f"Failed to execute '{task.name}' at '{timeNow}'. '{e!s}'.") LOG.error(f"Failed to execute '{task.name}' at '{timeNow}'. '{e!s}'.")
await self._notify.emit( await self._notify.emit(
Events.LOG_ERROR, Events.LOG_ERROR,
data={"preset": task.preset},
title="Task failed", title="Task failed",
message=f"Failed to execute '{task.name}'. '{e!s}'", message=f"Failed to execute '{task.name}'. '{e!s}'",
) )

View file

@ -15,18 +15,21 @@ async def add_url(queue: DownloadQueue, notify: EventBus, sid: str, data: dict):
await notify.emit(Events.LOG_ERROR, title="Invalid request", message="No URL provided.", to=sid) await notify.emit(Events.LOG_ERROR, title="Invalid request", message="No URL provided.", to=sid)
return return
item: Item = Item.format(data)
try: try:
status = await queue.add(item=Item.format(data)) status = await queue.add(item=item)
await notify.emit( await notify.emit(
event=Events.ITEM_STATUS, event=Events.ITEM_STATUS,
title="Adding URL", title="Adding URL",
message=f"Adding URL '{url}' to the download queue.", message=f"Adding URL '{url}' to the download queue.",
data=status, data={**status, "preset": item.preset} if status else {"preset": item.preset},
to=sid, to=sid,
) )
except ValueError as e: except ValueError as e:
LOG.exception(e) LOG.exception(e)
await notify.emit(Events.LOG_ERROR, title="Error Adding URL", message=str(e), to=sid) await notify.emit(
Events.LOG_ERROR, data={"preset": item.preset}, title="Error Adding URL", message=str(e), to=sid
)
@route(RouteType.SOCKET, "item_cancel", "item_cancel") @route(RouteType.SOCKET, "item_cancel", "item_cancel")

View file

@ -48,7 +48,7 @@
</button> </button>
</div> </div>
</div> </div>
<span class="help"> <span class="help is-bold">
<span class="icon"><i class="fa-solid fa-info" /></span> <span class="icon"><i class="fa-solid fa-info" /></span>
<span>You can use this field to populate the data, using shared string.</span> <span>You can use this field to populate the data, using shared string.</span>
</span> </span>
@ -63,7 +63,7 @@
<input type="text" class="input" id="name" v-model="form.name" :disabled="addInProgress" required> <input type="text" class="input" id="name" v-model="form.name" :disabled="addInProgress" required>
<span class="icon is-small is-left"><i class="fa-solid fa-user" /></span> <span class="icon is-small is-left"><i class="fa-solid fa-user" /></span>
</div> </div>
<span class="help"> <span class="help is-bold">
<span class="icon"><i class="fa-solid fa-info" /></span> <span class="icon"><i class="fa-solid fa-info" /></span>
<span>The notification target name, this is used to identify the target in the logs and <span>The notification target name, this is used to identify the target in the logs and
notifications.</span> notifications.</span>
@ -81,11 +81,13 @@
required> required>
<span class="icon is-small is-left"><i class="fa-solid fa-link" /></span> <span class="icon is-small is-left"><i class="fa-solid fa-link" /></span>
</div> </div>
<span class="help"> <span class="help is-bold">
<span class="icon"><i class="fa-solid fa-info" /></span> <span class="icon"><i class="fa-solid fa-info" /></span>
<span class="is-bold">The URL to send the notification to. It can be regular http/https endpoint. <span>
or <NuxtLink target="blank" href="https://github.com/caronc/apprise?tab=readme-ov-file#readme"> The URL to send the notification to. It can be regular http/https endpoint. or <NuxtLink
Apprise</NuxtLink> URL.</span> target="blank" href="https://github.com/caronc/apprise?tab=readme-ov-file#readme">Apprise
</NuxtLink> URL.
</span>
</span> </span>
</div> </div>
</div> </div>
@ -105,7 +107,7 @@
</div> </div>
<span class="icon is-small is-left"><i class="fa-solid fa-tv" /></span> <span class="icon is-small is-left"><i class="fa-solid fa-tv" /></span>
</div> </div>
<span class="help"> <span class="help is-bold">
<span class="icon"><i class="fa-solid fa-info" /></span> <span class="icon"><i class="fa-solid fa-info" /></span>
<span> <span>
The request method to use when sending the notification. This can be any of the standard HTTP The request method to use when sending the notification. This can be any of the standard HTTP
@ -130,7 +132,7 @@
</div> </div>
<span class="icon is-small is-left"><i class="fa-solid fa-tv" /></span> <span class="icon is-small is-left"><i class="fa-solid fa-tv" /></span>
</div> </div>
<span class="help"> <span class="help is-bold">
<span class="icon"><i class="fa-solid fa-info" /></span> <span class="icon"><i class="fa-solid fa-info" /></span>
<span> <span>
The request type to use when sending the notification. This can be <code>JSON</code> or The request type to use when sending the notification. This can be <code>JSON</code> or
@ -140,7 +142,7 @@
</div> </div>
</div> </div>
<div class="column is-12-mobile" :class="{ 'is-6-tablet': !isApprise, 'is-12': isApprise }"> <div class="column is-6-tablet is-12-mobile">
<div class="field"> <div class="field">
<label class="label is-inline" for="on"> <label class="label is-inline" for="on">
Select Events Select Events
@ -158,7 +160,7 @@
</div> </div>
<span class="icon is-small is-left"><i class="fa-solid fa-paper-plane" /></span> <span class="icon is-small is-left"><i class="fa-solid fa-paper-plane" /></span>
</div> </div>
<span class="help"> <span class="help is-bold">
<span class="icon"><i class="fa-solid fa-info" /></span> <span class="icon"><i class="fa-solid fa-info" /></span>
<span> <span>
Subscribe to the events you want to listen for. When the event is triggered, the notification will Subscribe to the events you want to listen for. When the event is triggered, the notification will
@ -169,7 +171,43 @@
</div> </div>
</div> </div>
<div class="column is-6-tablet is-12-mobile" v-if="!isApprise"> <div class="column is-6-tablet is-12-mobile">
<div class="field">
<label class="label is-inline" for="on">
Select Presets
<template v-if="form.presets.length > 0">
- <NuxtLink @click="form.presets = []">Clear selection</NuxtLink>
</template>
</label>
<div class="control has-icons-left">
<div class="select is-multiple is-fullwidth">
<select id="on" class="is-fullwidth" v-model="form.presets" :disabled="addInProgress" multiple>
<optgroup label="Custom presets" v-if="config?.presets.filter(p => !p?.default).length > 0">
<option v-for="item in filter_presets(false)" :key="item.id" :value="item.name">
{{ item.name }}
</option>
</optgroup>
<optgroup label="Default presets">
<option v-for="item in filter_presets(true)" :key="item.id" :value="item.name">
{{ item.name }}
</option>
</optgroup>
</select>
</div>
<span class="icon is-small is-left"><i class="fa-solid fa-sliders" /></span>
</div>
<span class="help is-bold">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>
Select the presets you want to listen for. If you select presets, only events that reference those
presets will trigger the notification. If no presets are selected, the notification will be sent
for all presets.
</span>
</span>
</div>
</div>
<div class="column is-12" v-if="!isApprise">
<div class="field"> <div class="field">
<label class="label is-inline" for="data_key"> <label class="label is-inline" for="data_key">
Data field Data field
@ -179,7 +217,7 @@
:disabled="addInProgress" required> :disabled="addInProgress" required>
<span class="icon is-small is-left"><i class="fa-solid fa-key" /></span> <span class="icon is-small is-left"><i class="fa-solid fa-key" /></span>
</div> </div>
<span class="help"> <span class="help is-bold">
<span class="icon"><i class="fa-solid fa-info" /></span> <span class="icon"><i class="fa-solid fa-info" /></span>
<span> <span>
The field name to use when sending the notification. This is used to identify the data in the The field name to use when sending the notification. This is used to identify the data in the
@ -206,7 +244,7 @@
<span class="icon is-small is-left"><i class="fa-solid fa-key" /></span> <span class="icon is-small is-left"><i class="fa-solid fa-key" /></span>
</div> </div>
</div> </div>
<span class="help"> <span class="help is-bold">
<span class="icon"><i class="fa-solid fa-info" /></span> <span class="icon"><i class="fa-solid fa-info" /></span>
<span>The header key to send with the notification.</span> <span>The header key to send with the notification.</span>
</span> </span>
@ -219,7 +257,7 @@
<span class="icon is-small is-left"><i class="fa-solid fa-v" /></span> <span class="icon is-small is-left"><i class="fa-solid fa-v" /></span>
</div> </div>
</div> </div>
<span class="help"> <span class="help is-bold">
<span class="icon"><i class="fa-solid fa-info" /></span> <span class="icon"><i class="fa-solid fa-info" /></span>
<span>The header value to send with the notification.</span> <span>The header value to send with the notification.</span>
</span> </span>
@ -234,7 +272,7 @@
</div> </div>
</template> </template>
</div> </div>
<span class="help"> <span class="help is-bold">
<span class="icon"><i class="fa-solid fa-exclamation" /></span> <span class="icon"><i class="fa-solid fa-exclamation" /></span>
<span class="has-text-danger"> <span class="has-text-danger">
If header key or value is empty, the header will not be sent. If header key or value is empty, the header will not be sent.
@ -274,6 +312,7 @@ import type { notification, notificationImport } from '~/types/notification'
const emitter = defineEmits(['cancel', 'submit']) const emitter = defineEmits(['cancel', 'submit'])
const toast = useNotification() const toast = useNotification()
const box = useConfirm() const box = useConfirm()
const config = useConfigStore()
const props = defineProps({ const props = defineProps({
reference: { reference: {
@ -396,7 +435,17 @@ const importItem = async () => {
if (item.on) { if (item.on) {
form.on = item.on form.on = item.on
}
if (item.presets) {
item.presets.forEach(p => {
if (!config.presets.find(cp => cp.name === p)) {
return
}
if (!form.presets.includes(p)) {
form.presets.push(p)
}
})
} }
import_string.value = '' import_string.value = ''
@ -407,4 +456,5 @@ const importItem = async () => {
} }
const isApprise = computed(() => form.request.url && !form.request.url.startsWith('http')) const isApprise = computed(() => form.request.url && !form.request.url.startsWith('http'))
const filter_presets = (flag: boolean = true) => config.presets.filter(item => item.default === flag)
</script> </script>

View file

@ -46,7 +46,7 @@
</div> </div>
<div class="is-hidden-mobile"> <div class="is-hidden-mobile">
<span class="subtitle"> <span class="subtitle">
Send notifications to your servers based on specified events. Send notifications to your webhooks based on specified events or presets.
</span> </span>
</div> </div>
</div> </div>
@ -76,15 +76,20 @@
<tbody> <tbody>
<tr v-for="item in notifications" :key="item.id"> <tr v-for="item in notifications" :key="item.id">
<td class="is-text-overflow is-vcentered"> <td class="is-text-overflow is-vcentered">
<div> <div class="is-bold">
{{ item.request.method.toUpperCase() }}({{ ucFirst(item.request.type) }}) @ {{ item.request.method.toUpperCase() }}({{ ucFirst(item.request.type) }}) @
<NuxtLink target="_blank" :href="item.request.url">{{ item.name }}</NuxtLink> <NuxtLink target="_blank" :href="item.request.url">{{ item.name }}</NuxtLink>
</div> </div>
<div class="is-unselectable"> <div class="is-unselectable">
<span class="icon-text"> <span class="icon-text">
<span class="icon"><i class="fa-solid fa-list-ul" /></span> <span class="icon"><i class="fa-solid fa-list-ul" /></span>
<span>On: {{ join_events(item.on) }}</span> <span>On: {{ join_events(item.on) }}</span>
</span> </span>
&nbsp;
<span class="icon-text">
<span class="icon"><i class="fa-solid fa-sliders" /></span>
<span>Presets: {{ join_presets(item.presets) }}</span>
</span>
</div> </div>
</td> </td>
<td class="is-vcentered is-items-center"> <td class="is-vcentered is-items-center">
@ -139,6 +144,10 @@
<span class="icon"><i class="fa-solid fa-list-ul" /></span> <span class="icon"><i class="fa-solid fa-list-ul" /></span>
<span>On: {{ join_events(item.on) }}</span> <span>On: {{ join_events(item.on) }}</span>
</p> </p>
<p>
<span class="icon"><i class="fa-solid fa-sliders" /></span>
<span>Presets: {{ join_presets(item.presets) }}</span>
</p>
<p v-if="item.request?.headers && item.request.headers.length > 0"> <p v-if="item.request?.headers && item.request.headers.length > 0">
<span class="icon"><i class="fa-solid fa-heading" /></span> <span class="icon"><i class="fa-solid fa-heading" /></span>
<span>{{item.request.headers.map(h => h.key).join(', ')}}</span> <span>{{item.request.headers.map(h => h.key).join(', ')}}</span>
@ -186,14 +195,21 @@
to ensure that the exported data does not contain any sensitive information for sharing. to ensure that the exported data does not contain any sensitive information for sharing.
</li> </li>
<li> <li>
When you set the request type as <code>Form</code>, the event data will be JSON encoded and sent as the When you set the request type as <code>Form</code>, the event data will be JSON encoded and sent as
and sent as <code>...&data_key=json_string</code>, only the <code>data</code> field will be JSON encoded. <code>...&data_key=json_string</code>, only the <code>data</code> field will be JSON encoded.
The other keys <code>id</code>, <code>event</code> and <code>created_at</code> will be sent as they are. The other keys <code>id</code>, <code>event</code> and <code>created_at</code> will be sent as they are.
</li> </li>
<li>We also send two special headers <code>X-Event-ID</code> and <code>X-Event</code> with the request.</li> <li>We also send two special headers <code>X-Event-ID</code> and <code>X-Event</code> with the request.</li>
<li>Support for <code>Apprise URLs</code> is in beta and subject to many changes to come, currently the <li>Support for <code>Apprise URLs</code> is in beta and subject to many changes to come, currently the
message field fallback to JSON encoded string of the event if there there is custom message set by us for message field fallback to JSON encoded string of the event if no custom message set by us for that
that particular event.</li> particular event.</li>
<li>
If you have selected specific presets or events, this will take priority, For example, if you limited the
target to <code>default</code> preset and selected <code>ALL</code> events, only events that reference the
<code>default</code> preset will be sent to that target. Like wise, if you have limited both events and
presets, then ONLY events that satisfy both conditions will be sent to that target. Only the
<code>test</code> events can bypass these conditions.
</li>
</ul> </ul>
</Message> </Message>
</div> </div>
@ -213,17 +229,15 @@ const isMobile = useMediaQuery({ maxWidth: 1024 })
const allowedEvents = ref<string[]>([]) const allowedEvents = ref<string[]>([])
const notifications = ref<notification[]>([]) const notifications = ref<notification[]>([])
const target = ref<notification>({
const defaultState = (): notification => ({
name: '', name: '',
on: [], on: [],
request: { presets: [],
method: 'POST', request: { method: 'POST', url: '', type: 'json', headers: [], data_key: 'data' },
url: '',
type: 'json',
headers: [],
data_key: '',
},
}) })
const target = ref<notification>(defaultState())
const targetRef = ref<string | null>('') const targetRef = ref<string | null>('')
const toggleForm = ref(false) const toggleForm = ref(false)
const isLoading = ref(false) const isLoading = ref(false)
@ -274,17 +288,7 @@ const reloadContent = async (fromMounted = false) => {
} }
const resetForm = (closeForm = false) => { const resetForm = (closeForm = false) => {
target.value = { target.value = defaultState()
name: '',
on: [],
request: {
method: 'POST',
url: '',
type: 'json',
headers: [],
data_key: '',
},
}
targetRef.value = null targetRef.value = null
if (closeForm) { if (closeForm) {
toggleForm.value = false toggleForm.value = false
@ -331,13 +335,7 @@ const deleteItem = async (item: notification) => {
toast.success('Notification target deleted.') toast.success('Notification target deleted.')
} }
const updateItem = async ({ const updateItem = async ({ reference, item }: { reference: string | null, item: notification }) => {
reference,
item,
}: {
reference: string | null;
item: notification;
}) => {
if (reference) { if (reference) {
const index = notifications.value.findIndex(i => i?.id === reference) const index = notifications.value.findIndex(i => i?.id === reference)
if (0 <= index) { if (0 <= index) {
@ -369,8 +367,8 @@ const editItem = (item: notification) => {
toggleForm.value = true toggleForm.value = true
} }
const join_events = (events: string[]) => const join_events = (events: Array<string>) => !events || events.length < 1 ? 'ALL' : events.map(e => ucFirst(e)).join(', ')
!events || events.length < 1 ? 'ALL' : events.map(e => ucFirst(e)).join(', ') const join_presets = (presets: Array<string>) => !presets || presets.length < 1 ? 'ALL' : presets.map(e => ucFirst(e)).join(', ')
const sendTest = async () => { const sendTest = async () => {
if (true !== (await box.confirm('Send test notification?'))) { if (true !== (await box.confirm('Send test notification?'))) {

View file

@ -15,7 +15,8 @@ type notification = {
id?: string; id?: string;
name: string; name: string;
request: notificationRequest; request: notificationRequest;
on: string[]; on: Array<string>;
presets: Array<string>;
raw?: boolean; raw?: boolean;
}; };