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
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:
commit
c528e2b557
7 changed files with 197 additions and 76 deletions
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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}'",
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
<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?'))) {
|
||||||
|
|
|
||||||
3
ui/app/types/notification.d.ts
vendored
3
ui/app/types/notification.d.ts
vendored
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue